Controlling editing in NSTableView - macos

I have subclassed NSTableView and NSTableColumn, and I want to control processus of edition of the cells of the table.
I have 2 states for my table class: edition and nonedition.
whatever the state of the table, when the user select a row, the row has to be overlighted.
if the table is in state edition, the controls corresponding to the selected row must be visible.
I use the function rowview to make this possible, but I have no success.
1 when I click on the text cell of a cell, the control appears always.
2 when the rowview function is called, I don't understand the value of the row parameter. Sometimes it is correct, sometimes it is incorrect.
is it someone who could hep me?

I want to make precision: to simplyfy, in my subclassed NSTableView, I write this code:
override func rowView(atRow row: Int, makeIfNecessary: Bool) -> NSTableRowView? {
let aRow = super.rowView (atRow: row, makeIfNecessary: makeIfNecessary)
if aRow != nil {
Swift.print("rowview is called for row (row)")
}
return aRow
}
When I click on row 0, I have the following result: "rowview is called for row 1"
Then when I click on row 1: "rowview is called for row 1 rowview is called for row 0 rowview is called for row 2"

Related

tableView(tableView, viewFor:,: Int) strange behavior

I'm trying to craft a view based tableView delegate method to alter a column's font IFF its identifier is 'name' and its value matches some global; otherwise the the nib should prevail. I think I have either a noob setup issue or my logic is omitting something.
I have two tables, one view based (tag=0), one cell (tag=1), and both have the same view controller as their delegate. Each tableView's has a unique tag and identifier, an array controller for column value bindings, and each of fits columns has an explicit unique identifier - matching its binding; I had tried omitting this - "the automatic setting" - but this yielded no data!?
So, I added this method, but both tableViews call into it; I would expect only the first. The 2nd table is a detail of the 1st, having its 1st array controller selection.
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let item = ((tableView.dataSource as! NSArrayController).arrangedObjects as! [AnyObject])[row]
var view = tableView.makeView(withIdentifier: tableColumn!.identifier, owner: self)
if view == nil {
Swift.print("tag: \(tableView.tag) ident: \(tableColumn!.identifier.rawValue)")
view = NSTableCellView.init()
view?.identifier = tableColumn?.identifier
}
guard tableView.tag == 0 else { return view }
guard isGlobalPlist, item.name == '<some-global-string>',
let identifier = tableColumn?.identifier,
identifier.rawValue == "name" else { return view }
if let cellView = view as? NSTableCellView {
if tableView.selectedRowIndexes.intersection(IndexSet.init(integer: row)).count > 0 {
cellView.textField?.font = .boldSystemFont(ofSize: -1)
cellView.textField?.textColor = .white
}
else
{
cellView.textField?.font = .systemFont(ofSize: -1)
cellView.textField?.textColor = .blue
}
}
return view
}
Firstly I notice that the method is called for the 2nd table when I wouldn't have expect it, it being a cell based tableView.
Anyway, I do create the cell when nil return, the docs cite that you need just need to matchup the identifiers:
Note that a cell view’s identifier must be the same as its table column’s identifier for bindings to work. If you’re using bindings, it’s recommended that you use the Automatic identifier setting in Interface Builder.
which I do; both tableView's columns are bound (view, or cell) to their respective array controller using column identifier explicitly entered.
Anyway the original intent I was after was for the view based (1st) tableView:
the view controller was controlling a global resource (isGlobalPlist is true)
the row's 'name' value matches a global value value
to the affect 'name' cell shown in distinct color (blue/yellow - or something) depending on whether the row was selected due to selection highlighting, dark mode etc conditions.
So there would be 1 of 4 possibilities
selected row, matching global -> fore:yellow, back:default
selected row, not matching global -> fore:default, back:default
non-selected row, matching global -> fore:blue, back:default
non-selected row, not matching global -> fore:default, back:default
If the target row matched requirements, the coloring needs to vary depending on its inclusion in the tableView's selected indexes.
The setting seems to properly affect initially - selected row and matching value, but then settings seem to bleed across other rows navigated to - so the selection change. At most there should be 1 row's name column with this affect.
I can't tell if my logic or setup is wrong; I suspect I probably need to refresh as I move about ?
Well I gave up on a view based solution but this fills my need; a cell based solution:
func tableView(_ tableView: NSTableView, dataCellFor tableColumn: NSTableColumn?, row: Int) -> NSCell? {
guard let column = tableColumn else { return nil }
let item : AnyObject = ([table0ArrayController,table1ArrayController][tableView.tag]?.arrangedObjects as! [AnyObject])[row]
let cell : NSTextFieldCell = column.dataCell(forRow: row) as! NSTextFieldCell
cell.textColor = .textColor
guard tableView.tag == 0, isGlobalPlist, item.name == '<some-global-string>' else { return cell }
if tableView.selectedRowIndexes.intersection(IndexSet.init(integer: row)).count > 0 {
cell.textColor = .white
}
else
{
cell.textColor = .blue
}
return cell
}
I wanted to call attention to specific row(s) with color, unless row was selected for the 'name' column only.

NSTextField Loses Focus When Neighboring NSTableView Reloaded

I have a search field (NSTextField) called searchField and when you type in it, it refreshes the data shown in a NSTableView. The problem is that this refresh also triggers the selection of a table row, and that takes the focus out of the NSTextField.
The user types in the search field:
func controlTextDidChange(_ obj: Notification) {
if let field = obj.object as? NSTextField, field == searchField{
refreshData()
}
}
Then the NSTableView gets reloaded here:
func refreshData(){
//Process search term and filter data array
//...
//Restore previously selected row (if available)
let index = tableView.selectedRow
tableView.reloadData()
//Select the previously selected row
tableView.selectRowIndexes(NSIndexSet(index: index) as IndexSet, byExtendingSelection: false)
}
If I comment-out both the reloadData() and the selectRowIndexes then the search field behaves as intended (I can keep typing and it keeps the focus in the field). But if include either or both of those methods, the search field loses focus after I type the first character and refreshData() is called.
How can I keep focus in my search field and not let the table reload hijack the focus?
Ugh... it turns out I could never get the NSTableView to let go of the focus because it wasn't set to allow an Empty selection state. Checking a box in Interface Builder fixed it.

get row number of dragged row in tableview

i have two table views where you can drag a row from table A to table B.
this works fine.
but i need to know, which row number will dragged from table A.
This is my code of table A and the problem, too:
func tableView(_ tableView: NSTableView, writeRowsWith rowIndexes: IndexSet, to pboard: NSPasteboard) -> Bool {
pboard.declareTypes([NSStringPboardType],owner: nil)
pboard.setString(persons[0].objectID.uriRepresentation().absoluteString, forType: NSStringPboardType)
return true
}
at the moment i always dragged the first element of "persons"
i need a solution where i can set a row number (or something like this) instead of the static "persons[0]".
is there an way for this?
Have you tried persons[rowindexes.first] ?

NSTableView - Initial Selection Grey until Clicked (Focussed)

I've got a simple example of an app here which I slapped together, and what I'm getting is pretty much what I'm after.
The issue is that when the view loads up, in the NSViewController's viewDidLoad, I set the tableView's selected index to 0 i.e. the first item (which works).
What I do notice is that when this happens, the selected row comes up as grey in color (i.e. as if it's not an active window/view)… It only seems to high light in the normal blue color when I physically click on the row that's selected.
I can confirm that the row is selected and everything appears fine.
Any ideas?
To confirm, the code I use to select the row is:
override func viewDidAppear() {
self.tableView.selectRowIndexes(NSIndexSet(index: 0), byExtendingSelection: false)
}
Here is what's happening with the actual view itself:
ABOVE: The darker grey line is the "selection bar". This is what happens as soon as the view becomes active.
ABOVE: Once I click on that row (the one which was once dark grey), I get he desired high lighting.. i.e. Navy Blue.
The reason why the cell is grey is because the table view doesn't have focus / isn't the first responder.
There are 3 states for tableView cell selection color
no selection = clear row background
selection and focus = blue row background
selection and no focus = grey row background
This is probably because another view has focus. Simply selecting a cell doesn't shift focus to a tableView. You need to call NSWindow.makeFirstResponder() to change the focus.
func tableViewSelectionDidChange(notification: NSNotification) {
let tableView = notification.object as! NSTableView
if tableView.selectedRow != -1 {
self.window!.makeFirstResponder(self.tableView)
}
}
I've managed to find out what's going on. (I think) and it seems to work.
I had to:
Subclass NSTableRowView
Add a new NSView just below the actual cell view (row) in Interface Builder
Set the new Row View's class to 'myNSTableViewSubClass'
Set the row view's Identifier to: NSTableViewRowViewKey (this is very specific, and that literally is the key, if this isn't set, it won't work be regarded as the Table Row View.
in the subclass I had to override the emphasised: Bool to always return yes e.g.:
override var emphasized: Bool{
get{
return true
}
set{
//You need to have the "set" there as it's a mutable prop
//It doesn't have to do untying though
}
}
And voila..
The catch in my case was in 4 above.

Xcode 7 ui automation - loop through a tableview/collectionview

I am using xCode 7.1. I would like to automate interaction with all cells from a table/collection view. I would expect it to be something like this:
for i in 0..<tableView.cells.count {
let cell = collectionView.cells.elementBoundByIndex(i)
cell.tap()
backBtn.tap()
}
However this snippet only queries current descendants of the table view, so it will loop through the first m (m < n) loaded cells out of total n cells from the data source.
What is the best way to loop through all cells available in data source? Obviously querying for .Cell descendants is not the right approach.
P.S.: I tried to perform swipe on table view after every tap on cell. However it swipes to far away (scrollByOffset is not available). And again, don't know how to extract total number of cells from data source.
Cheers,
Leonid
So problem here is that you cannot call tap() on a cell that is not visible. SoI wrote a extension on XCUIElement - XCUIElement+UITableViewCell
func makeCellVisibleInWindow(window: XCUIElement, inTableView tableView: XCUIElement) {
var windowMaxY: CGFloat = CGRectGetMaxY(window.frame)
while 1 {
if self.frame.origin.y < 0 {
tableView.swipeDown()
}
else {
if self.frame.origin.y > windowMaxY {
tableView.swipeUp()
}
else {
break
}
}
}
}
Now you can use this method to make you cell visible and than tap on it.
var window: XCUIElement = application.windows.elementBoundByIndex(0)
for i in 0..<tableView.cells.count {
let cell = collectionView.cells.elementBoundByIndex(i)
cell.makeCellVisibleInWindow(window, inTableView: tableView)
cell.tap()
backBtn.tap()
}
let cells = XCUIApplication().tables.cells
for cell in cells.allElementsBoundByIndex {
cell.tap()
cell.backButton.tap()
}
I face the same situation however from my trials, you can do tap() on a cell that is not visible.
However it is not reliable and it fails for an obscur reason.
It looks to me that this is because in some situation the next cell I wanted to scroll to while parsing my table was not loaded.
So here is the trick I used:
before parsing my tables I first tap in the last cell, in my case I type an editable UITextField as all other tap will cause triggering a segue.
This first tap() cause the scroll to the last cell and so the loads of data.
then I check my cells contents
let cells = app.tables.cells
/*
this is a trick,
enter in editing for last cell of the table view so that all the cells are loaded once
avoid the next trick to fail sometime because it can't find a textField
*/
app.tables.children(matching: .cell).element(boundBy: cells.count - 1).children(matching: .textField).element(boundBy: 0).tap()
app.typeText("\r") // exit editing
for cellIdx in 0..<cells.count {
/*
this is a trick
cell may be partially or not visible, so data not loaded in table view.
Taping in it is will make it visible and so do load the data (as well as doing a scroll to the cell)
Here taping in the editable text (the name) as taping elsewhere will cause a segue to the detail view
this is why we just tap return to canel name edidting
*/
app.tables.children(matching: .cell).element(boundBy: cellIdx).children(matching: .textField).element(boundBy: 0).tap()
app.typeText("\r")
// doing my checks
}
At least so far it worked for me, not sure this is 100% working, for instance on very long list.

Resources