How to implement a NSButton like the Finder button - swift

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:

Related

ContextMenu issue in UIBarButtonItem

I put UIBarButtonItem inside Toolbar as you can see in Storyboard. I put UIMenu in UIBarButtonItem.menu. When I click a button in UIMenu, the icon of the button I clicked is transferred to UIBarButtonItem. Why is this happening?
Question:
Why does the UIBarButtonItem's icon change when I press the button from ContextMenu?
Video:
Storyboard:
UIBarButtonItem Attributes:
ViewController:
#IBOutlet weak var fooItem: UIBarButtonItem!
//MARK: Functions
override func viewDidLoad() {
super.viewDidLoad()
fooItem.menu = ContextMenuManager.shared.makeUIMenu()
}
ContextMenuManager:
protocol ContextMenuFunctionable {
func didTappedEmptyFolder()
func didTappedImportFile()
}
class ContextMenuManager {
static var shared = ContextMenuManager()
var delegate: ContextMenuFunctionable?
var emptyFolder: UIAction?
var importFile: UIAction?
var importPhotoOrVideo: UIAction?
private init() {
emptyFolder = UIAction(
title: "Empty Folder",
image: .folderBadgePlus
.applyingSymbolConfiguration(.symbolConfig),
identifier: nil,
state: .off,
handler: { _ in self.delegate?.didTappedEmptyFolder() }
)
importFile = UIAction(
title: "Import File",
image: .squareAndArrowDown.applyingSymbolConfiguration(.symbolConfig),
identifier: nil,
state: .off,
handler: { _ in self.delegate?.didTappedImportFile() }
)
importPhotoOrVideo = UIAction(
title: "Import Photo & Video",
image: .photo.applyingSymbolConfiguration(.symbolConfig),
identifier: nil,
state: .off,
handler: { _ in self.delegate?.didTappedImportFile() }
)
}
func makeUIMenu() -> UIMenu {
guard let emptyFolder, let importFile, let importPhotoOrVideo else { return UIMenu() }
return UIMenu(title: "Add",
image: .folder.applyingSymbolConfiguration(.symbolConfig),
options: [.displayInline, .singleSelection],
children: [importPhotoOrVideo, importFile, emptyFolder])
}
}
Uncheck the "Selection as Primary Action" checkbox and the button's icon should no longer change when you make a menu selection.
I replicated what you are seeing in code. I created a menu basically using your ContextMenuManager code but I created the UIBarButtonItem as follows:
let menu = UIMenu(title: "Add",
options: [.displayInline, .singleSelection],
children: [importPhotoOrVideo, importFile, emptyFolder])
let button = UIBarButtonItem(image: UIImage(systemName: "doc.on.clipboard.fill"), menu: menu)
button.changesSelectionAsPrimaryAction = false
When changesSelectionAsPrimaryAction is set to true, the button's icon reflects the chosen menu. When set to false, it retains its own icon.

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)

second level menu NSView to link NSMenuItem problems

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

swift mac osx NSButton is not responding until a long press

I am having a weird issue with a button. So I have a NSViewController with many subviews in it. When I click a button, a new NSView with click gestures and buttons is added on top. But I can't press any of them, they don't respond unless a click for 2 seconds and then release. I've tried disabling the gestures of the holder but it didn't work. Any suggestions?
Well, some of the rest of us do. In my case, it's for buttons on a view in a sheet, so "many subviews" isn't likely it. My view controller for the sheet is about 100 lines. Still debugging...
At present the VC is as follows. The snp.makeConstraints calls are for SnapKit (from GitHub)
#objc
class ThreadEditSheetViewController: NSViewController {
/// The container for the graphics view
#IBOutlet var sheetView: NSView!
/// The information packet initialized by the invoking view controller
var info: ThreadEditInfo!
/// API
override func viewDidLoad() {
super.viewDidLoad()
}
/// API
override func viewWillAppear() {
guard let gvc = (try? self.bundleLoader(id: "GraphicsViewController")) as? GraphicsViewController else {
fatalUserAlert(error: AppError.UIConstructionFailure, message: "Can't find GraphicsViewController for ThreadEditSheetViewController")}
let gv = gvc.view
self.view.addSubview(gv)
// Spaces in title text move it left to avoid visual overlap with scroll bar. Don't know how to do it with
// constraints given the scrolling view
let done = makeButton(gvc: gvc, title: "done ", action: #selector(doneEditing(_:)))
done.snp.makeConstraints{ (make) in
make.top.equalTo(gv).offset(-5)
make.right.equalTo(gv).offset(-5)
}
let cancel = makeButton(gvc: gvc, title: "cancel", action: #selector(cancelEditing(_:)))
cancel.snp.makeConstraints{ (make) in
make.top.equalTo(gv).offset(-5)
make.left.equalTo(gv).offset(5)
}
self.view.becomeFirstResponder()
super.viewWillAppear()
return
}
func makeButton(gvc: NSViewController, title: String, action: Selector) -> NSButton {
let button = NSButton(title: title, target: self, action: action)
let gv = gvc.view
gv.addSubview(button)
button.backgroundColor = .clear
button.setButtonType(.momentaryChange)
button.isTransparent = true
return button
}
#objc
func doneEditing(_ sender: Any) {
self.dismissViewController(self)
}
#objc
func cancelEditing(_ sender: Any) {
self.dismissViewController(self)
}
}

NSMenu selector in Swift

I'm at a loss to see why this doesn't work. The menu shows, but is grayed out if I leave autoenablesItems at the default, and the actions aren't called if I set it false.
class GameScene: SKScene {
// ...
func action1(sender: AnyObject) {
println("Urk, action 1")
}
func action2(sender: AnyObject) {
println("Urk, action 2")
}
func popUpMenu(#event: NSEvent) {
var theMenu = NSMenu(title: "Contextual menu")
theMenu.addItemWithTitle("Action 1", action: Selector("action1:"), keyEquivalent: "")
theMenu.addItemWithTitle("Action 2", action: Selector("action2:"), keyEquivalent: "")
//theMenu.autoenablesItems = false
NSMenu.popUpContextMenu(theMenu, withEvent:event, forView:self.view)
}
override func mouseDown(theEvent: NSEvent) {
self.popUpMenu(event: theEvent) // The menu shows
}
}
Update
As per #Chuck's answer, you will need to do the following:
func popUpMenu(#event: NSEvent) {
var theMenu = NSMenu(title: "Contextual menu")
theMenu.addItemWithTitle("Action 1", action: Selector("action1:"), keyEquivalent: "")
theMenu.addItemWithTitle("Action 2", action: Selector("action2:"), keyEquivalent: "")
for item: AnyObject in theMenu.itemArray {
if let menuItem = item as? NSMenuItem {
menuItem.target = self
}
}
NSMenu.popUpContextMenu(theMenu, withEvent:event, forView:self.view)
}
It sounds like your problem is that an NSMenuItem created with that method doesn't have a receiver, so it uses the responder chain, and this object is not in the responder chain. You can force it to see your object by setting the menu items' targets to self.