NSOutlineView how to show blue outline during right click - cocoa

Everyone discussed about how to get rid of the blue outline during right click... but me.
Instead, I'm trying to display the blue outline.
I didn't get any outline when I right clicked my outline view row. The menu appeared but the outline wasn't. You can see that the blue outline is not visible in this picture below:
Below is what I'm trying to achieve.
Update
This is how I implemented the NSMenu. I subclassed the NSOutlineView and made a new protocol to override NSOutlineViewDelegate.
This idea was to make it simple by letting the NSOutlineView ask the NSMenu for each item, so we can implement different menu for each item. It works but the blue outline view doesn't show up during right click.
KRMenuOutlineView.swift
import Cocoa
#objc protocol KRMenuOutlineViewDelegate: NSOutlineViewDelegate {
// This method will ask NSMenu for each item in outline view
func outlineView(_ outlineView: KRMenuOutlineView, menuFor item: Any, event: NSEvent) -> NSMenu?
}
class KRMenuOutlineView: NSOutlineView {
override var delegate: NSOutlineViewDelegate? {
didSet {
if let newValue = delegate {
/*
* Swift doesn't support overriding inherited properties with different type
* like Objective C Does, therefore we need internal delegate.
*/
internalDelegate = unsafeBitCast(newValue, to: KRMenuOutlineViewDelegate.self)
} else {
internalDelegate = nil
}
}
}
private var internalDelegate: KRMenuOutlineViewDelegate?
override func menu(for event: NSEvent) -> NSMenu? {
let point = self.convert(event.locationInWindow, from: nil)
if let item = self.item(atRow: self.row(at: point)) {
return self.internalDelegate?.outlineView(self, menuFor: item, event: event)
}
return super.menu(for: event)
}
}
Then, I use it in my view controller like this:
KRTreeViewController.swift
extension KRTreeViewController: KRMenuOutlineViewDelegate {
func outlineView(_ outlineView: KRMenuOutlineView, menuFor item: Any, event: NSEvent) -> NSMenu? {
let menu = NSMenu(title: "Contextual Menu")
menu.delegate = self
let key = String(utf16CodeUnits: [unichar(NSBackspaceCharacter)], count: 1) as String
let deleteMenuItem = menu.addItem(withTitle: "Delete",
action: #selector(didClickMenuItem(_:)),
keyEquivalent: key)
deleteMenuItem.representedObject = myItem
deleteMenuItem.target = self
return menu
}
#objc fileprivate func didClickMenuItem(_ menuItem: NSMenuItem) {
// ...
}
}

How to properly show a context menu:
If you have created your menu using a storyboard:
First, go to the storyboard and add the menu to the viewController that contains the outlineView.
Then make it an #IBOutlet so you can reference it later.
In a method like viewDidLoad(), add the menu to the outlineView by calling
outlineView.menu = myMenu
where myMenu can either be the one you created in Interface Builder or in code.
You can run the app now and should see the blue outline around the cell.
The problem now is that you don't know which cell the user has clicked.
To fix this, set yourself as the delegate of myMenu and adopt the NSMenuDelegate protocol.
func menuNeedsUpdate(_ menu: NSMenu) {
let row = self.outlineView.clickedRow
guard row != -1 else { return }
for item in menu.items {
item.representedObject = row
}
}
Here you can do whatever you need. This implementation sets the rowIndex as the representedObject of each menu item. Keep in mind that this only works on static outlineViews (ones that don't change in the background) and menus which only go one level deep.
You could also store the index or object represented by the cell (if the outlineView is not static) in a local variable.

Related

Enable or disable menu items on different view controllers in cocoa app?

I have 3 view controllers say main1, main2 and child. I have added a menu item, on click of that it should open child view controller as modal.
Whenever user is in main1 VC, menu item should be enabled. If user in main2 VC, menu should be disabled. Right now I’ve added modal segue between menu item and child VC.
I followed following approaches to disable, but they are not working.
Method 1:
In main2 VC, I’ve added
func validateUserInterfaceItem(_ anItem: NSValidatedUserInterfaceItem) -> Bool {
return false
}
override func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
return false
}
Method 2:
override func viewDidLoad() {
super.viewDidLoad()
let mainMenu = NSApplication.shared().mainMenu!
let appMenu = mainMenu.item(at: 0)!.submenu
appMenu?.item(withTitle: someMenuTitle)?.isEnabled = false
}
If you use a modal segue it will be always activated.
To enable/disable dependent on the presented view controller I would add an action to the view controller to open the view controller manualy as modal. The menu item has to be connected to the action (openModalViewController) with the first responder.
#IBAction func openModalViewController(_ sender: AnyObject) {
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let viewController = storyboard.instantiateController(withIdentifier: "MyViewController") as! NSViewController
presentAsModalWindow(viewController)
}
Consider there must be at least one view able to get the first responder in main1/main2 that the menu item will activate. If this is not the case you would have to implement acceptsFirstResponder for the corresponding view.
override var acceptsFirstResponder: Bool{
return true
}
To implement validateUserInterfaceItem would be not required in this case, only if you want to control activation/deactivation dependent on an additional state as in the example below.
extension ViewController: NSMenuItemValidation {
func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
if menuItem.action == #selector(delete(_:)) {
return tableView.selectedRow < 0 ? false : true
}
return true
}
}

How to detect a Pan Gesture inside a NSTouchBarView

Is it possible to detect a finger pan on a NSTouchBarView?
Sorry for the lack of code but I don't even know where to start.
MacOS is not made for finger touches but the TouchBar is but I do not see how to do it on a NSTouchBarView
I don't know specifically about using NSTouchBarView, but using a pan recognizer in a touch bar usually works like this: create a view, then create a NSPanGestureRecognizer (don't forget to set the target and action) then add the recognizer to the previously created view. Finally, create your NSCustomTouchBarItem and assign the previously created view to the item's view. Quick example in Swift:
func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItemIdentifier) -> NSTouchBarItem? {
switch identifier {
case NSTouchBarItemIdentifier.yourCustomItem:
return itemWithRecognizer(identifier: identifier)
default:
return nil
}
}
func itemWithRecognizer(identifier: NSTouchBarItemIdentifier) -> NSTouchBarItem {
let customView = NSView()
customView.wantsLayer = true
let recognizer = NSPanGestureRecognizer()
recognizer.target = self
recognizer.action = #selector(doSomething)
customView.addGestureRecognizer(recognizer)
let item = NSCustomTouchBarItem(identifier: identifier)
item.view = customView
return item
}
func doSomething() {
// gesture was activated
}

Align NSToolbarItems with NSSplitView columns

Finder and Notes have a peculiar behaviour that I am seeking to reproduce. The ‘flexible space’ in the NSToolbar seems to take the dimensions of the split view into account. For instance, the first group of buttons aligns on the left side with the right side of the sidebar. The second group of icons aligns with the right side of the first column. When I widen the sidebar, the toolbar items move along with it.
Is it possible to reproduce this?
Solution
With the solution provided by #KenThomases, I have implemented this as follows:
final class MainWindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
window?.toolbar?.delegate = self
// Make sure that tracking is enabled when the toolbar is completed
DispatchQueue.main.async {
self.trackSplitViewForFirstFlexibleToolbarItem()
}
}
}
extension MainWindowController: NSToolbarDelegate {
func toolbarWillAddItem(_ notification: Notification) {
// Make sure that tracking is evaluated only after the item was added
DispatchQueue.main.async {
self.trackSplitViewForFirstFlexibleToolbarItem()
}
}
func toolbarDidRemoveItem(_ notification: Notification) {
trackSplitViewForFirstFlexibleToolbarItem()
}
/// - Warning: This is a private Apple method and may break in the future.
func toolbarDidReorderItem(_ notification: Notification) {
trackSplitViewForFirstFlexibleToolbarItem()
}
/// - Warning: This method uses private Apple methods that may break in the future.
fileprivate func trackSplitViewForFirstFlexibleToolbarItem() {
guard var toolbarItems = self.window?.toolbar?.items, let splitView = (contentViewController as? NSSplitViewController)?.splitView else {
return
}
// Add tracking to the first flexible space and remove it from the group
if let firstFlexibleToolbarItem = toolbarItems.first, firstFlexibleToolbarItem.itemIdentifier == NSToolbarFlexibleSpaceItemIdentifier {
_ = firstFlexibleToolbarItem.perform(Selector(("setTrackedSplitView:")), with: splitView)
toolbarItems.removeFirst()
}
// Remove tracking from other flexible spaces
for flexibleToolbarItem in toolbarItems.filter({ $0.itemIdentifier == NSToolbarFlexibleSpaceItemIdentifier }) {
_ = flexibleToolbarItem.perform(Selector(("setTrackedSplitView:")), with: nil)
}
}
}
When using macOS 11 or newer, you can insert NSTrackingSeparatorToolbarItem items to the toolbar, which will split up your toolbar in sections, aligned with the dividers of a NSSplitView object.
This example adds the new separator items to a toolbar that already contains the rest of the buttons, configured in Interface Builder or in code. The target splitview concerns a standard configuration of 3 splitviews, including a sidebar panel.
class WindowController: NSWindowController, NSToolbarDelegate {
let mainPanelSeparatorIdentifier = NSToolbarItem.Identifier(rawValue: "MainPanel")
override func windowDidLoad() {
super.windowDidLoad()
self.window?.toolbar?.delegate = self
// Calling the inserts async gives more time to bind with the split viewer, and prevents crashes
DispatchQueue.main.async {
// The .sidebarTrackingSeparator is a built-in tracking separator which always aligns with the sidebar splitview
self.window?.toolbar?.insertItem(withItemIdentifier: .sidebarTrackingSeparator, at: 0)
// Example of a custom mainPanelSeparatorIdentifier
// Index at '3' means that there are 3 toolbar items at the left side
// of this separator, including the first tracking separator
self.window?.toolbar?.insertItem(withItemIdentifier: mainPanelSeparatorIdentifier at: 3)
}
}
func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
if let splitView = (self.contentViewController as? NSSplitViewController)?.splitView {
// You must implement this for custom separator identifiers, to connect the separator with a split view divider
if itemIdentifier == mainPanelSeparatorIdentifier {
return NSTrackingSeparatorToolbarItem(identifier: itemIdentifier, splitView: splitView, dividerIndex: 1)
}
}
return nil
}
}
If you want to add an extra separator, for example for an Inspector panel, simply insert an additional toolbar item identifier to the toolbar, and assign an extra NSTrackingSeparatorToolbarItem to another divider in the itemForItemIdentifier delegate function.
You can do this with Apple-private methods, although that's not allowed in the App Store.
There's a private method, -setTrackedSplitView:, on NSToolbarItem. It takes an NSSplitView* as its parameter. You need to call it on the flexible-space toolbar item that you want to track a split view and pass it the split view it should track. To protect yourself against Apple removing the method, you should check if NSToolbarItem responds to the method before trying to use it.
Since the user can customize and re-order the toolbar, you generally need to enumerate the window's toolbar's items. For the first one whose identifier is NSToolbarFlexibleSpaceItemIdentifier, you set the split view it should track. For all other flexible-space items, you clear (set to nil) the split view to track. You need to do that when the window is first set up and again in the toolbar delegate's -toolbarWillAddItem: and -toolbarDidRemoveItem: methods. There's also another undocumented delegate method, -toolbarDidReorderItem:, where I've found it useful to update the toolbar.

Xcode_OSX/Swift_NSPopUpButton.

I am incredibly new to this, so please keep that in mind!
I've been at this all night, watched countless videos/haunted
countless forums...I can't find one single answer!
I am trying to make a basic popup menu in Swift/OSX What I need to figure out is:
How can I add more than the 'three items' to this menu
Whatever is selected in the popup, for that info to send an integer
value to another number.
I very much would appreciate your help, Thanks.
A NSPopupButton is a container for a bunch of NSMenuItem objects so to add an item you can use
func addItemWithTitle(_ title: String!)
The NSMenuItem gets constructed for you by the call.
and as you may wish to start from scratch you can use
func removeAllItems()
To clean existing items from the button.
There are also other methods around moving and removing menu items from the button.
A NSPopupButton is-a NSControl so you can use var action: Selector to set the action sent when an item is selected and var target: AnyObject! to control which object receives the message. Or just wire it up in Interface Builder.
protocol FooViewDelegate{
func itemWithIndexWasSelected(value:Int)
}
class FooViewController: NSViewController {
#IBOutlet weak var myPopupButton: NSPopUpButton!
var delegate: FooViewDelegate?
let allTheThings = ["Mother", "Custard", "Axe", "Cactus"]
override func viewDidLoad() {
super.viewDidLoad()
buildMyButton()
}
func buildMyButton() {
myPopupButton.removeAllItems()
myPopupButton.addItemsWithTitles(allTheThings)
myPopupButton.target = self
myPopupButton.action = "myPopUpButtonWasSelected:"
}
#IBAction func myPopUpButtonWasSelected(sender:AnyObject) {
if let menuItem = sender as? NSMenuItem, mindex = find(allTheThings, menuItem.title) {
self.delegate?.itemWithIndexWasSelected(mindex)
}
}
}
All the button construction can be done in Interface Builder rather than code too. Remember that you can duplicate items with CMD-D or you can drag new NSMenuItem objects into the button.

How to get selected item of NSOutlineView without using NSTreeController?

How do I get the selected item of an NSOutlineView with using my own data source.
I see I can get selectedRow but it returns a row ID relative to the state of the outline. The only way to do it is to track the expanded collapsed state of the items, but that seems ridiculous.
I was hoping for something like:
array = [outlineViewOutlet selectedItems];
I looked at the other similar questions, they dont seem to answer the question.
NSOutlineView inherits from NSTableView, so you get nice methods such as selectedRow:
id selectedItem = [outlineView itemAtRow:[outlineView selectedRow]];
Swift 5
NSOutlineView has a delegate method outlineViewSelectionDidChange
func outlineViewSelectionDidChange(_ notification: Notification) {
// Get the outline view from notification object
guard let outlineView = notification.object as? NSOutlineView else {return}
// Here you can get your selected item using selectedRow
if let item = outlineView.item(atRow: outlineView.selectedRow) {
}
}
Bonus Tip: You can also get the parent item of the selected item like this:
func outlineViewSelectionDidChange(_ notification: Notification) {
// Get the outline view from notification object
guard let outlineView = notification.object as? NSOutlineView else {return}
// Here you can get your selected item using selectedRow
if let item = outlineView.item(atRow: outlineView.selectedRow) {
// Get the parent item
if let parentItem = outlineView.parent(forItem: item){
}
}
}
#Dave De Long: excellent answer, here is the translation to Swift 3.0
#objc private func onItemClicked() {
if let item = outlineView.item(atRow: outlineView.clickedRow) as? FileSystemItem {
print("selected item url: \(item.fileURL)")
}
}
Shown is a case where item is from class FileSystemItem with a property fileURL.

Resources