As the title says, when my status bar menu is open and a NSAlert is triggered from another thread, the UI freezes.
Presumably this is because both things are running on the main thread. But since I'm dealing with an NSAlert and an NSMenu, don't I have to run these on the main thread?
NSAlert Code
func showWallpaperUpdateErrorAlert(messageText: String, informativeText: String) {
DispatchQueue.main.async {
NSApp.activate(ignoringOtherApps: true)
let updateErrorAlert = NSAlert()
updateErrorAlert.messageText = messageText
updateErrorAlert.informativeText = informativeText
updateErrorAlert.addButton(withTitle: "OK")
updateErrorAlert.runModal()
}
}
NSMenu Code
func createStatusBarMenu() {
// Status bar icon
guard let icon = NSImage(named: "iconFrame44")
else { NSLog("Error setting status bar icon image."); return }
icon.isTemplate = true
statusBarItem.image = icon
// Create Submenu items
let viewOnRedditMenuItem = NSMenuItem(title: "View on Reddit...", action: #selector(viewOnRedditAction), keyEquivalent: "")
viewOnRedditMenuItem.target = self
let saveThisImageMenuItem = NSMenuItem(title: "Save This Image...", action: #selector(saveThisImageAction), keyEquivalent: "")
saveThisImageMenuItem.target = self
// Add to title submenu
let titleSubmenu = NSMenu(title: "")
titleSubmenu.addItem(descriptionMenuItem)
titleSubmenu.addItem(NSMenuItem.separator())
titleSubmenu.addItem(viewOnRedditMenuItem)
titleSubmenu.addItem(saveThisImageMenuItem)
// Create main menu items
titleMenuItem = NSMenuItem(title: "No Wallpaperer Image", action: nil, keyEquivalent: "")
titleMenuItem.submenu = titleSubmenu
titleMenuItem.isEnabled = false
getNewWallpaperMenuItem = NSMenuItem(title: "Update Now", action: #selector(getNewWallpaperAction), keyEquivalent: "")
getNewWallpaperMenuItem.target = self
let preferencesMenuItem = NSMenuItem(title: "Preferences...", action: #selector(preferencesAction), keyEquivalent: "")
preferencesMenuItem.target = self
let quitMenuItem = NSMenuItem(title: "Quit Wallpaperer", action: #selector(quitAction), keyEquivalent: "")
quitMenuItem.target = self
// Add to main menu
let statusBarMenu = NSMenu(title: "")
statusBarMenu.addItem(titleMenuItem)
statusBarMenu.addItem(NSMenuItem.separator())
statusBarMenu.addItem(getNewWallpaperMenuItem)
statusBarMenu.addItem(NSMenuItem.separator())
statusBarMenu.addItem(preferencesMenuItem)
statusBarMenu.addItem(quitMenuItem)
statusBarItem.menu = statusBarMenu
statusBarMenu.delegate = self
}
In my case the solution was to dismiss the menu before showing the alert.
I had to access the menu from the NSStatusItem's menu property and call cancelTrackingWithoutAnimation() (regular cancelTracking() wasn't as smooth). I also had to do this outside the main thread, for whatever reason.
func showWallpaperUpdateErrorAlert(messageText: String, informativeText: String) {
statusBarItem.menu?.cancelTrackingWithoutAnimation() // This is new
DispatchQueue.main.async {
NSApp.activate(ignoringOtherApps: true)
let updateErrorAlert = NSAlert()
updateErrorAlert.messageText = messageText
updateErrorAlert.informativeText = informativeText
updateErrorAlert.addButton(withTitle: "OK")
updateErrorAlert.runModal()
}
}
Related
Please refer to the screenshot, I want to implement a NSButton like the buttons on Finder window. It seems that the button style is similar with a NSButton recessed. I have tried the recessed button but it is a little different from the Finder button, even the one which have a drop down menu. I have no idea how to make a drop down menu with recessed button.
If I choose to make a bevel style borderless NSButton with a special image, I cannot set a highlighted hover background whose size is larger than the image inside. The hover area is always the same with the image.
So any body known how to implement this. Thanks
Use NSMenuToolbarItem instead and check the inline comment in the sample source:
Assign this custom class MainWindowController to the initial NSWindowController controller:
class MainWindowController: NSWindowController {
// `NSMenuToolbarItem` Items
var dropDownMenu: NSMenu = {
var menu = NSMenu(title: "DropDown")
let item1 = NSMenuItem(title: "Item 1", action: nil, keyEquivalent: "")
let item2 = NSMenuItem(title: "Item 1", action: nil, keyEquivalent: "")
let item3 = NSMenuItem(title: "Item 3", action: nil, keyEquivalent: "")
menu.items = [item1, item2, item3]
return menu
}()
override func windowDidLoad() {
super.windowDidLoad()
configureToolbar()
}
private func configureToolbar() {
if let safeWindow = self.window {
let toolbar = NSToolbar(identifier: "mainWindowToolbar")
toolbar.delegate = self
toolbar.displayMode = .default
safeWindow.toolbarStyle = .automatic
safeWindow.toolbar = toolbar
safeWindow.toolbar?.validateVisibleItems()
}
}
}
extension MainWindowController: NSToolbarDelegate {
func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
let toolbarItem = NSMenuToolbarItem(itemIdentifier: itemIdentifier)
toolbarItem.showsIndicator = true // To display DropDown Indicator
toolbarItem.menu = self.dropDownMenu
toolbarItem.isBordered = true // Show ToolBar Item Background on Mouse Hover
toolbarItem.image = NSImage(systemSymbolName: "ellipsis.circle", accessibilityDescription: "")
return toolbarItem
}
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [NSToolbarItem.Identifier("DropDownAction")]
}
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [NSToolbarItem.Identifier("DropDownAction")]
}
}
Output:
I have a menu with menu items. The problem is that my menu items are all greyed out or not enabled
public override init() {
super.init()
let menu = NSMenuItem(title: "Debug", action: nil, keyEquivalent: "")
menu.submenu = NSMenu(title: "Debug")
menu.submenu?.addItem(withTitle: "Load saved data", action: #selector(loadDataFromFile(_:)), keyEquivalent: "");
menu.submenu?.addItem(withTitle: "another item", action: #selector(loadDataFromFile(_:)), keyEquivalent: "")
menu.isEnabled = true
NSApplication.shared.mainMenu?.addItem(menu)
}
#objc func loadDataFromFile(_ sender: Any) {
print("load it")
}
To be able to call a custom selector in the current class you have to set the target of the menu item to self
let menu = NSMenuItem(title: "Debug", action: nil, keyEquivalent: "")
menu.submenu = NSMenu(title: "Debug")
let loadItem = NSMenuItem(title: "Load saved data", action: #selector(loadDataFromFile), keyEquivalent: "")
loadItem.target = self
let anotherItem = NSMenuItem(title: "another item", action: #selector(loadDataFromFile), keyEquivalent: "")
anotherItem.target = self
menu.submenu?.addItem(loadItem)
menu.submenu?.addItem(anotherItem)
menu.isEnabled = true
The application is background only
This is a timer app
A dialog box(NSAlert) pops up after a specified time to prompt the user. How to ensure NSAlert on window top?
StatusBarMenu
class StatusBarMenu: NSObject {
private let statusItem: NSStatusItem!
init(statusItem: NSStatusItem) {
self.statusItem = statusItem
}
private func createMenu() {
selft.addMenuItem()
}
func refresh() {
self.statusItem.menu?.removeAllItems()
self.createMenu()
}
func addMenuItem() {
let item = NSMenuItem(title: "show alert", action: #selector(self.showAlert), keyEquivalent: "")
item.target = self
self.statusItem.menu?.addItem(item)
}
#objc func showAlert() {
DispatchQueue.main.sync {
let alert = NSAlert()
alert.icon = NSImage(named: "Alert")
alert.messageText = title
alert.informativeText = text
alert.alertStyle = .informational
alert.addButton(withTitle: "ok")
alert.runModal()
}
}
}
I found an answer to my own question:
alert.window.level = .floating
This works.
This is an example of how to keep NSAlert window on top
alert = NSAlert.alertWithMessageText_defaultButton_alternateButton_otherButton_informativeTextWithFormat_(
title, ok, cancel, other, message)
alert.setAlertStyle_(0) # informational style
# customize# floating window
alert.window().setLevel_(3)
The first and second level menus are matched one by one through the NSmenuItem's title invocation array, and the first level menu also works properly to display data.
But once the secondary menu into NSView, only the first item will be show the data, the rest are blank, tried a lot of methods, did not succeed, please guide me, how to deal with?
update method:
func updateData() {
for city in cites {
let airQualityApi = AirQualityApi()
airQualityApi.fetchData(query: city, success: {airQuality in
if let airQualityMenyItem = self.statusMenu.item(withTitle: city){
airQualityMenyItem.title = airQuality.description
self.airQualityDetailView.update(airQuality: airQuality)
}
})
}
}
Sec level NSView update method:
func update(airQuality: AirQuality) {
DispatchQueue.main.sync {
self.cityName.stringValue = airQuality.city
self.airQuality.intValue = Int32(airQuality.aqi)
}
}
add NSMenuItem:
override func awakeFromNib() {
updateData()
let icon = NSImage(named: "icon")
icon?.isTemplate = true
if let button = statusItem.button {
button.image = icon
}
statusItem.menu = statusMenu
//let editMenuItem = NSMenuItem()
//editMenuItem.title = "showAqi"
for i in 0 ... cites.count - 1 {
let mainMenu = NSMenuItem(title: cites[i], action: .none, keyEquivalent: "")
statusMenu.insertItem(mainMenu, at: i)
let sub = NSMenu()
let subMenu = NSMenuItem(title: mainMenu.title, action: .none, keyEquivalent: "")
subMenu.view = airQualityDetailView
//print(subMenu.title)
statusMenu.setSubmenu(sub, for: mainMenu)
sub.addItem(subMenu)
//sub.insertItem(subMenu, at: 0)
}
}
you can see the integral code here 👉 the whole project code on GitHub
thank you soooooo much
🙏🙏🙏🙏🙏🙏🙏🙏🙏
I'm currently working on a "menu bar only" project and I need to list all mounted Volumes in a submenu in the menubar app. I figured out how to print() all mounted volumes but I need help with the submenu (without .xib or .storyboard work)
This is my "listVolumes func"
func listVolumes(sender: NSMenuItem) {
let keys = [NSURLVolumeNameKey, NSURLVolumeIsRemovableKey, NSURLVolumeIsEjectableKey]
let paths = NSFileManager().mountedVolumeURLsIncludingResourceValuesForKeys(keys, options: [])
if let urls = paths {
for url in urls {
if let components = url.pathComponents
where components.count > 1
&& components[1] == "Volumes" {
print(url)
}
}
}
}
an below is my code for the "menu bar app"
let statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(-2)
func applicationDidFinishLaunching(aNotification: NSNotification) {
if let button = statusItem.button {
button.image = NSImage(named: "StatusBarButtonImage")
}
let menu = NSMenu()
menu.addItem(NSMenuItem(title: "Volumes", action: Selector("volumes:"), keyEquivalent: ""))
menu.addItem(NSMenuItem.separatorItem())
menu.addItem(NSMenuItem(title: "Help", action: Selector("help:"), keyEquivalent: ""))
menu.addItem(NSMenuItem.separatorItem())
menu.addItem(NSMenuItem(title: "Quit", action: Selector("terminate:"), keyEquivalent: "q"))
statusItem.menu = menu
}
so my question is, how can I create a submenu that contains all the mounted drives (programmatically only)
You have to set a submenu to the volumes menu item for example
let volumesMenuItem = NSMenuItem(title: "Volumes", action: Selector("volumes:"), keyEquivalent: "")
menu.addItem(volumesMenuItem)
let volumesMenu = NSMenu(title: "Volumes")
volumesMenuItem.submenu = volumesMenu
I recommend to return an array of the names from your listVolumes() function, then you can add menu items to volumesMenu assuming volumes contains the names
for volumeName in volumes {
volumesMenu.addItem(NSMenuItem(title: volumeName, action: Selector("selectVolume"), keyEquivalent: ""))
}