NSTextField Loses Focus When Neighboring NSTableView Reloaded - cocoa

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.

Related

Preselecting a NSComboBox Entry

I have a modal window that contains a NSComboBox. "Uses Data Source" is set to turn and the combobox correctly lists the entries. The view controller is the delegate for the data source. When I call the modal window to update an existing object I would like the combobox to display the selected entry if there is one. How do I do this?
I've tried to access the entries in viewWillAppear. I get and error saying there are no entries. The various print statements I have in the code indicate that the values aren't loaded until the drop down arrow is clicked. The two function I'm supplying as the delegate are:
func numberOfItems(in comboBox: NSComboBox) -> Int
func comboBox(_ comboBox: NSComboBox, objectValueForItemAt index: Int) -> Any?
Would it be possible to set the selected entry in the second function above?
As NSComboBox is a subclass of NSTextField you should be able to set its text by setting the stringValue property of your comboBox.
self.comboBox.stringValue = "Hello World"

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.

How to send a key event to an NSTextField that does not have focus in swift

I have a view that contains an NSTableView and an NSTextField. The table view has focus. I would like to use the text field as a filter for the table view. Is there any way that I can send a key press event, which is captured by the table view, to the NSTextField?
Here is the keyDown func as I have it, I would like to send theEvent to the text field in the default handler of the switch statement.
override func keyDown(theEvent: NSEvent) {
let s = theEvent.charactersIgnoringModifiers!
let s1 = s.unicodeScalars
let s2 = s1[s1.startIndex].value
let s3 = Int(s2)
switch s3 {
case NSUpArrowFunctionKey:
self.previous()
return
case NSDownArrowFunctionKey:
self.next()
return
default:
// this appends the text to the textField, Is there a way to send theEvent to the textField?
textField.stringValue = textField.stringValue + s;
// textField.keyDown(theEvent) // this does not work
break
}
}
I would like to not have to force the user to change focus to the text field in order to filter the table view results.
As far as I understand it the -keyDown: is sent to the firstResponder which in this case is the table view.
I don't like this idea and I would implement this is a different way, but you could try inserting the text field into the responder chain and a position above table view. You would have to subclass the text field and implement mouseDown to capture the events and you would also have to change you table views mouse down implementation so it didn't capture the event for the text field. This is overly complicated.
Why not simply set the target/action of the text field and deal with user input outside of the event loop?

NSTextField in NSTableCellView - end editing on loss of focus

I have a view with a view-based NSTableView (which itself has a cell view with a single text field) and some buttons and textfields outside the tableview. One of the buttons adds an object into the datasource for the tableview, and after inserting the row into the tableview, immediately makes it editable.
If the user enters the text and pressed the return key, I receive the - (BOOL)control:(NSControl *)control textShouldEndEditing:(NSText *)fieldEditor delegate method fine, and I can run my validation and save the value. But the delegate doesn't get called if the user selects any of the other buttons or textfields outside the tableview.
What's the best way to detect this loss-of-focus on the textfield inside the NSTableCellView, so I can run some of my validation code on the tableview entry?
If I understand you correctly you want a control:textShouldEndEditing: notification to fire in the following situation:
You add a new object to the array controller.
The row in the table representing the object is automatically selected.
YOU programmatically select the text field in the relevant row for editing.
The user immediately (i.e. without making any edits in the text field) gives focus to a control elsewhere in the UI
One approach I've used in the past to get this working is to make an insignificant programmatic change to the field editor associated with the text field, just before the text field becomes available to the user for editing. The snippet below shows how to do this - this is step 2/step 3 in the above scenario:
func tableViewSelectionDidChange(notification: NSNotification) {
if justAddedToArrayController == true {
// This change of selection is occurring because the user has added a new
// object to the array controller, and it has been automatically selected
// in the table view. Now need to give focus to the text field in the
// newly selected row...
// Access the cell
var cell = tableView.viewAtColumn(0,
row: arrayController.selectionIndex,
makeIfNecessary: true) as NSTableCellView
// Make the text field associated with the cell the first responder (i.e.
// give it focus)
window.makeFirstResponder(cell.textField!)
// Access, then 'nudge' the field editor - make it think it's already
// been edited so that it'll fire 'should' messages even if the user
// doesn't add anything to the text field
var fe = tableView.window?.fieldEditor(true, forObject: cell.textField!)
fe!.insertText(cell.textField!.stringValue)
}
}

Resources