NSTableView Change cell color when selected - macos

This is a macOS question
In
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?
I'm using
func tableViewSelectionDidChange(_ notification: Notification) {
cell.layer?.backgroundColor = mintGreen.cgColor
to change the cell color.
Is it possible to change the color when the cell is selected?
Thanks

Did you ever get anywhere with this?
Realise this is an old question now, but I couldn't find a solution to this problem documented anywhere, so this is what I've come up with:
This code assumes an NSTableView with one column and therefore that the cell you are interested is in the zero indexed column. It also assumes a reference to the NSTableView on the delegate of 'theTableView'.
func tableViewSelectionDidChange(_ notification: Notification) {
// You'd probably want to handle this else statement more elegantly
guard let selected = theTableView?.selectedRow else { return }
if (selected != -1) {
if let numberOfRows = theTableView?.numberOfRows {
let sequenceOfRows = 0..<numberOfRows
for row in sequenceOfRows.reversed() {
if row == selected {
if let cell = theTableView?.view(atColumn: 0, row: selected, makeIfNecessary: false) {
cell.layer?.backgroundColor = NSColor.green.cgColor
}
} else {
if let cell = theTableView?.view(atColumn: 0, row: Int(row), makeIfNecessary: false) {
cell.layer?.backgroundColor = NSColor.clear.cgColor
}
}
}
}
} else {
// No row is selected, so iterate over all and revert background to .clear
if let numberOfRows = theTableView?.numberOfRows {
let sequenceOfRows = 0..<numberOfRows
for row in sequenceOfRows {
if let cell = theTableView?.view(atColumn: 0, row: Int(row), makeIfNecessary: false) {
cell.layer?.backgroundColor = NSColor.clear.cgColor
}
}
}
}
}
This works, and technically answers the question, but feels really horrible, having to iterate over all the rows to assess which background colour to change, and to change all the rows back again when the row selection changes or the row is deselected. I can't help feel there must be many more elegant or efficient ways to do this, but as I say can't find any documented myself.
Would be grateful if anyone else could update if there is another more succinct way of doing this, or if there is another pattern aside from the NSTableView delegates to achieve this.

Have you tried setting the row view using a subclass of NSTableRowView. You can override drawing in the subclass. or simply set the rows background colour based on the item.
func outlineView(_ outlineView: NSOutlineView, rowViewForItem item: Any) -> NSTableRowView? {
switch (item as! NSTreeNode).representedObject {
case _ as FixtureNode:
return OSFixtureTableHeaderRowView()
default:
return nil
}
}
It took me a long time to figure this out so for anyone interested here is a short clip showing a custom outline view that shows grouped items in a box in the list view. I haven't figured out how to determine when the outline view loses focus to change the selection colours accordingly but for this application it's better that the user can easily see the current selection so there is no immediate requirement but if anyone figures that out let us know. Happy to post more details if anyone need help.
Custom NSOutlineView

Related

Is there a way to force reloadData on a UICollectionView using RxSwift?

I have a UICollectionView bound to an array of entities using BehaviorSubject and all is fine, data is loaded from the network and displayed correctly.
The problem is, based on user action, I'd like to change the CellType used by the UICollectionView and force the collection to re-create all cells, how do I do that?
My bind code looks like:
self.dataSource.bind(to: self.collectionView!.rx.items) {
view, row, data in
let indexPath = IndexPath(row: row, section: 0)
var ret: UICollectionViewCell? = nil
if (self.currentReuseIdentifier == reuseIdentifierA) {
// Dequeue cell type A and bind it to model
ret = cell
} else {
// Dequeue cell type B and bind it to model
ret = cell
}
return ret!
}.disposed(by: disposeBag)
The general way to solve problems in Rx is to think of what you want the output effect to be and what input effects can affect it.
In your case, the output effect is the display of the table view. You have identified two input effects "data is loaded from the network" and "user action". In order to make your observable chain work properly, you will have to combine your two input effects in some way to get the behavior you want. I can't say how that combination should take place without more information, but here is an article explaining most of the combining operators available: https://medium.com/#danielt1263/recipes-for-combining-observables-in-rxswift-ec4f8157265f
As a workaround, you can emit an empty list then an actual data to force the collectionView to reload like so:
dataSource.onNext([])
dataSource.onNext([1,2,3])
I think you can use different data type to create cell
import Foundation
import RxDataSources
enum SettingsSection {
case setting(title: String, items: [SettingsSectionItem])
}
enum SettingsSectionItem {
case bannerItem(viewModel: SettingSwitchCellViewModel)
case nightModeItem(viewModel: SettingSwitchCellViewModel)
case themeItem(viewModel: SettingCellViewModel)
case languageItem(viewModel: SettingCellViewModel)
case contactsItem(viewModel: SettingCellViewModel)
case removeCacheItem(viewModel: SettingCellViewModel)
case acknowledgementsItem(viewModel: SettingCellViewModel)
case whatsNewItem(viewModel: SettingCellViewModel)
case logoutItem(viewModel: SettingCellViewModel)
}
extension SettingsSection: SectionModelType {
typealias Item = SettingsSectionItem
var title: String {
switch self {
case .setting(let title, _): return title
}
}
var items: [SettingsSectionItem] {
switch self {
case .setting(_, let items): return items.map {$0}
}
}
init(original: SettingsSection, items: [Item]) {
switch original {
case .setting(let title, let items): self = .setting(title: title, items: items)
}
}
}
let dataSource = RxTableViewSectionedReloadDataSource<SettingsSection>(configureCell: { dataSource, tableView, indexPath, item in
switch item {
case .bannerItem(let viewModel),
.nightModeItem(let viewModel):
let cell = (tableView.dequeueReusableCell(withIdentifier: switchReuseIdentifier, for: indexPath) as? SettingSwitchCell)!
cell.bind(to: viewModel)
return cell
case .themeItem(let viewModel),
.languageItem(let viewModel),
.contactsItem(let viewModel),
.removeCacheItem(let viewModel),
.acknowledgementsItem(let viewModel),
.whatsNewItem(let viewModel),
.logoutItem(let viewModel):
let cell = (tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as? SettingCell)!
cell.bind(to: viewModel)
return cell
}
}, titleForHeaderInSection: { dataSource, index in
let section = dataSource[index]
return section.title
})
output.items.asObservable()
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: rx.disposeBag)
RxDataSources
swiftHub

NSOutlineView - Get NSTableCellView for a Given Item

My view-based outline view displays a custom context menu (right-click menu) for its rows.
One of the menu items on the menu is "Rename...", and the menu item's representedObject property is set to the object represented by the outline view row:
let menu = NSMenu()
// ...other menu items...
let renameItem = NSMenuItem(
title: "Rename...",
action: #selector(OutlineViewController.rename(_:)),
keyEquivalent: "")
renameItem.representedObject = object
menu.addItem(renameItem)
On the action side, I want to make the text field in the table cell editable, programmatically. The problem is, I am not sure of how to get a reference to the table cell from the represented object alone.
This is my action method:
#IBAction func rename(_ sender: Any) {
guard let menuItem = sender as? NSMenuItem else { return }
guard let item = menuItem.representedObject else { return }
I can get the row for the represented object (Int):
let row = outlineView.row(forItem: item)
...and the row view (NSTableRowView):
let rowView = outlineView.rowView(atRow: row, makeIfNecessary: false)
I can get the column index (Int) and column (NSTableColumn):
let columnIndex = outlineView.column(withIdentifier: "TitleColumn")
let column = outlineView.tableColumns[columnIndex]
...and attempt to get the cell view (NSTableCellView):
guard let cell = outlineView(outlineView, viewFor: column, item: item) as? NSTableCellView else {
return
}
Finally, I try to make the text field editable:
guard let textField = cell.textField else {
return
}
textField.becomeFirstResponder()
All these test pass (I set breakpoints), but nothing happens: The text field does not become editable (unlike when double clicking).
What am I doing wrong?
Edit I have realized that when I call:
outlineView(outlineView, viewFor: column, item: item)
This is basically the NSOutlineViewDelegate method that my view controller is implementing, so instead of giving me the cell already on screen, it is creating a new copy on demand. Kind of like calling
UITableViewController.tableView(_:,cellForRowAt:)
// Data source method, creates or dequeues a new cell to pass
// back to the table for display
...instead of calling:
UITableView.cellForRowAt(_:)
// Method of table view proper; returns existing cell if
// already on screen, or defers to the data source (method
// above) otherwise
So instead of calling a delegate method that creates the cell view anew, I should query the outline view itself. However, the only meaningful method I can see is:
func rowView(atRow row: Int, makeIfNecessary: Bool) -> NSTableRowView?
...which returns an NSTableRowView...
Solved it. Just had to dig a little deeper into the API:
// Get row index
let row = outlineView.row(forItem: item)
// Get view for the whole row:
guard let rowView = outlineView.rowView(atRow: row, makeIfNecessary: false) else {
return
}
// THIS is the missing piece: Get row subview for the given column
// (= cell)
guard let cell = rowView?.view(atColumn: 0) as? NSTableCellView else {
return
}
Now I have a reference to the actual text field already on screen, and it becomes editable:
self.view.window?.makeFirstResponder(cell.textField)

NSTableView old selection

How do I get the previous selected index of row in NSTableView
The methods
func tableViewSelectionIsChanging(notification: NSNotification) {
let index = (notification.object as! NSTableView).selectedRow
println(index)
}
func tableViewSelectionDidChange(notification: NSNotification)
let index = (notification.object as! NSTableView).selectedRow
println(index)
}
Both methods print same value. How do I get the index / row which is going to change due to selection ?
Or in simple terms, how do I make a statement like
println("\(Selection changed from \(tableView.oldSelectedIndex) to \(tableView.newSelectionIndex)")
Before a cell is selected, the function func tableView(tableView: NSTableView, shouldSelectRow row: Int) is called on the delegate. Here, you can see both the currently selected row, and the proposed selection.
func tableView(tableView: NSTableView, shouldSelectRow row: Int) -> Bool {
// This is the next index
NSLog("Next index: \(row)")
// Make sure that a row is currently selected!
guard self.tableView.selectedRowIndexes.count > 0 else {
return true
}
// Get the currently selected row.
let index = self.tableView.selectedRow
NSLog("Previous index: \(index)")
return true
}
NB: This doesn't exactly answer your question, because you've asked for the change after the new selection has occurred, whereas this gives it just before. I hope nonetheless that this is sufficient.
You can get it by
tableView.selectedRow

Setting NSTableView cell text

I have a NSTableView that I want to populate with 20 cells, each of them will say "Test". I'm fluent with UITableView's but not so much with NSTableViews, so I went hunting online to figure this out. Oh, how confused that made me! I understand that I need to use the numberOfRowsInTableView function, but how do I set the text of the cell? Each source I find seems to do everything in a different way. For instance, this site uses:
func tableView(tableView: NSTableView!, viewForTableColumn tableColumn: NSTableColumn!, row: Int) -> NSView! {
// 1
var cellView: NSTableCellView = tableView.makeViewWithIdentifier(tableColumn.identifier, owner: self) as NSTableCellView
// 2
if tableColumn.identifier == "BugColumn" {
// 3
let bugDoc = self.bugs[row]
cellView.imageView!.image = bugDoc.thumbImage
cellView.textField!.stringValue = bugDoc.data.title
return cellView
}
return cellView
}
I tried that but I got an error - the code found nil while unwrapping an optional. Then I tried what I found here:
func tableView(tableView: NSTableView, dataCellForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSCell? {
if tableColumn == nil { return nil }
if tableColumn!.identifier != "right" { return nil }
let cell = NSPopUpButtonCell()
cell.bordered = false
cell.menu!.addItemWithTitle("one", action: nil, keyEquivalent: "")
cell.menu!.addItemWithTitle("two", action: nil, keyEquivalent: "")
cell.menu!.addItemWithTitle("three", action: nil, keyEquivalent: "")
cell.selectItemAtIndex(1) // <--- obviously ignored ?!
return cell
}
So my question is, how do I set the cell text? How do the two examples I've inserted above differ in what they do? Please, make some sense of this - cause I sure can't!
-Thanks,
A confused CodeIt
P.S.
I've looked at several other sources except the two I named above. I'm just plain confused..
Edit:
The found nil while unwrapping an Optional error I mentioned in the first example is found on this line:
var cellView: NSTableCellView = tableView.makeViewWithIdentifier(tableColumn.identifier, owner: self) as NSTableCellView
// Get an existing cell with the MyView identifier if it exists
var cellView?: NSTableCellView = tableView.makeViewWithIdentifier("someIdentifier", owner: self) as NSTableCellView
// There is no existing cell to reuse so create a new one
if cellView == nil {
cellView = NSTableCellView(frame: NSRect())
// The identifier of the NSTextField instance is set to someIdentifier.
// This allows the cell to be reused.
cellView.identifier = "someIdentifier
}
This should give you the cell, and you can proceed.
See Apple Doc for more

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