second level menu NSView to link NSMenuItem problems - swift

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
🙏🙏🙏🙏🙏🙏🙏🙏🙏

Related

How to implement a NSButton like the Finder button

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:

How to ensure NSAlert pop up on window top?

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)

How to make app start at login or add to login items after install like dropbox? Xcode 11, Swift 5, MacOS 10.13+

I am trying to have my app start automatically when a user logs out and back in or after a reboot. The objective is to have the app there indefinitely unless a user closes out, but, to have it start back up when they reboot.
There are several tutorials I have followed and threads I have read but they all seem outdated. I am using this app on 3 different OS' including 10.13, 10.14 and 10.15. I seem to have most of the issues on 10.15 machines. I can't figure out why it is hit or miss whether the app starts on login and why sometimes it does not.
https://martiancraft.com/blog/2015/01/login-items/
https://theswiftdev.com/how-to-launch-a-macos-app-at-login/
I have code signed the application, sandbox is enabled, but maybe since the tutorials and information I have found is outdated I am missing something outside the code. This app is for internal company use only, and thus, I will not be submitting to the Apple. I intend to deploy to machines using our management software, and have built a .pkg to deploy the application + launcher into the applications folder and to run automatically after install.
Any help, cleanup, explanations or suggestions are welcome and appreciated.
Main App:
extension Notification.Name{
static let killLauncher = Notification.Name("killLauncher")
}
extension AppDelegate: NSApplicationDelegate{
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
//assign variables for launcherhelper
let launcherAppId = "Kinetic.KTGHelperLauncher"
let runningApps = NSWorkspace.shared.runningApplications
let isRunning = !runningApps.filter {$0.bundleIdentifier == launcherAppId }.isEmpty
//set launcher to login item
SMLoginItemSetEnabled(launcherAppId as CFString, true)
//status check if running or not running
if isRunning {
DistributedNotificationCenter.default().post(name: .killLauncher, object: Bundle.main.bundleIdentifier!)
}
//configure button to display button in assets
if let button = statusItem.button {
button.image = NSImage(named:NSImage.Name("kinetic_websitemain_red"))
}
//builds menu on start
constructMenu()
}
}
#NSApplicationMain
class AppDelegate: NSObject {
let statusItem = NSStatusBar.system.statusItem(withLength:NSStatusItem.squareLength)
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
#objc func TakeScreenshot(_ sender: Any){
//get path to user download folder
let dirPath = FileManager().urls(for:.downloadsDirectory, in:.userDomainMask)[0]
//create time stamp of when picture is taken
func CreateTimeStamp() -> Int32
{
return Int32(Date().timeIntervalSince1970)
}
var displayCount: UInt32 = 0;
var result = CGGetActiveDisplayList(0, nil, &displayCount)
if (result != CGError.success) {
print("error: \(result)")
return
}
let allocated = Int(displayCount)
let activeDisplays = UnsafeMutablePointer<CGDirectDisplayID>.allocate(capacity: allocated)
result = CGGetActiveDisplayList(displayCount, activeDisplays, &displayCount)
if (result != CGError.success) {
print("error: \(result)")
return
}
for i in 1...displayCount {
let unixTimestamp = CreateTimeStamp()
let fileUrl = dirPath.appendingPathComponent("\(unixTimestamp)" + "_" + "\(i)" + ".jpg", isDirectory:false)
let screenShot:CGImage = CGDisplayCreateImage(activeDisplays[Int(i-1)])!
let bitmapRep = NSBitmapImageRep(cgImage: screenShot)
let jpegData = bitmapRep.representation(using: NSBitmapImageRep.FileType.jpeg, properties: [:])!
do {
try jpegData.write(to: fileUrl, options: .atomic)
}
catch {print("error: \(error)")}
}
}
#objc func kineticSelf(_ sender: Any){
let kineticSelfUrl = URL(string: "/Library/Addigy/macmanage/MacManage.app")
NSWorkspace.shared.openFile(kineticSelfUrl!.path)
// NSWorkspace.shared.open(URL(fileURLWithPath: "/Library/Addigy/macmanage/MacManage.app"))
}
//function that opens kinetic helpdesk website
#objc func kineticHelpdesk(_ sender: Any){
let kineticHelpdeskUrl = URL(string: "http://helpdesk.kinetictg.com")!
NSWorkspace.shared.open(kineticHelpdeskUrl)
}
//function that takes user to teamviewer ktg site
#objc func kineticRemote(_ sender: Any){
let kineticRemoteUrl = URL(string: "https://get.teamviewer.com/ktgsupport")!
NSWorkspace.shared.open(kineticRemoteUrl)
}
//call kinetic
#objc func kineticHomepage(_ sender: Any){
let url = URL(string: "https://kinetictg.com")!
NSWorkspace.shared.open(url)
}
//function to build menu
func constructMenu(){
let menu = NSMenu()
//section for "Request Support"
menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: "Request Support", action: nil, keyEquivalent:""))
//support ticket
menu.addItem(NSMenuItem(title: "Support Ticket", action:
#selector(AppDelegate.kineticHelpdesk(_:)), keyEquivalent: ""))
//remote support
menu.addItem(NSMenuItem(title: "Remote Support", action:
#selector(AppDelegate.kineticRemote(_:)), keyEquivalent: ""))
//section for "Tools"
menu.addItem(NSMenuItem.separator( ))
menu.addItem(NSMenuItem(title: "Tools", action: nil, keyEquivalent:""))
//start agent installation audit
menu.addItem(NSMenuItem(title: "Take Screenshot", action:
#selector(AppDelegate.TakeScreenshot(_:)), keyEquivalent: ""))
//open self service
menu.addItem(NSMenuItem(title: "Open Self Service", action:
#selector(AppDelegate.kineticSelf(_:)), keyEquivalent: ""))
//Section for "Info"
menu.addItem(NSMenuItem.separator( ))
menu.addItem(NSMenuItem(title: "Info", action: nil, keyEquivalent:""))
//contact info
menu.addItem(NSMenuItem(title: "Kinetic Homepage", action:
#selector(AppDelegate.kineticHomepage(_:)), keyEquivalent: ""))
//quit app
menu.addItem(NSMenuItem(title: "Quit", action:
#selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
statusItem.menu = menu
}
}
Launcher Application:
import Cocoa
//extension variable for launcher to kill launcher
extension Notification.Name {
static let killLauncher = Notification.Name("killLauncher")
}
#NSApplicationMain
class HelperAppDelegate: NSObject {
//terminate object
#objc func terminate(){
NSApp.terminate(nil)
}
}
extension HelperAppDelegate: NSApplicationDelegate{
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
//main app identifier
let mainAppIdentifier = "Kinetic.KTG-Helper"
let runningApps = NSWorkspace.shared.runningApplications
let isRunning = !runningApps.filter { $0.bundleIdentifier == mainAppIdentifier }.isEmpty
//if app is running kill launcher entity and reset status of killlauncher
if !isRunning {
DistributedNotificationCenter.default().addObserver(self, selector: #selector(self.terminate), name: .killLauncher, object: mainAppIdentifier)
let path = Bundle.main.bundlePath as NSString
var components = path.pathComponents
components.removeLast(3)
components.append("MacOS")
components.append("KTG Helper")
let newPath = NSString.path(withComponents: components)
NSWorkspace.shared.launchApplication(newPath)
}
else{
self.terminate()
}
}
//func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
//}
}

Actions assigned to NSMenuItem dont seem to work

Heres whats going on:
I am attempting to build a Mac Status Bar App completely programmatically. Everything seems to be working fine, that is the menu shows up in the Mac status bar, the dropdown menu is displaying how it should. But when I click on the menu items, nothing happens. I even changed the target function to just doing the basic task of printing to the terminal, and nothing.
About the code:
The issue lies somewhere around here I think:
menu.addItem(NSMenuItem(title: val, action: #selector(toggleService), keyEquivalent: ""))
That code should fire off the > toggleService function. But it doesn't do anything. Could the issue be due to the fact that I am only inheriting from the NSObject class?
The Code
// StatusBar.swift
import Cocoa
class StatusBar: NSObject {
var menuButton = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
var menu = NSMenu()
var service = Service()
override init() {
super.init()
menuButton.button?.image = NSImage(named: NSImage.Name("icon"))
menuButton.menu = menu
menu.autoenablesItems = false
for (_, val) in service.list {
menu.addItem(NSMenuItem(title: val, action: #selector(toggleService), keyEquivalent: ""))
}
menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: "Quit", action: #selector(quit), keyEquivalent: ""))
}
#objc func toggleService(sender: NSMenuItem) {
print ("Say Something.. anything??")
}
#objc func quit(sender: NSMenuItem) {
print ("Say Something.. anything??")
}
}
menuItem.target = self
You need to set the target to 'self'. NSMenuItems have two basic requirements. An action, and a target,
Action
menuItem.action: #selector(YOURFUNCTION)
Target
menuItem.target = self
So to get your menu items working, replace the for loop (within your init call) with this new one:
for (_, val) in service.list {
let menuItem = menu.addItem(NSMenuItem(title: val, action: #selector(toggleService), keyEquivalent: ""))
menuItem.target = self
}

NSAlert appearing while NSMenu is open causes UI to freeze

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()
}
}