0
votes

Use Case:

This is a data entry scenario where the user needs to modify the data in three columns of a view based NSTableView; qty, item number and price. The objective is to keep the users hands on the keyboard and diminish the need for mouse'ing around the table to perform edits.

Detailed UI:

When tabbing within a row, the next column should become selected and editable which is (can be) the default behavior of a NSTableView. However, when in the last column (price) is being edited and tabbing out, it should wrap to the next available row, column 0 (qty) and allow the user to continue editing.

Likewise, backtab should be supported within a row, and when in the first column (qty) and backtabbing it should wrap up the the prior row, last column (price). If they are on row 0, column 0, it should wrap down to the last row, last column.

So essentially the user should be able to navigate and edit any row, column with tab or backtab presses.

While there are several solutions, using keyDown, keyUp, notifications etc a lot of it is older ObjC Code and not very Swifty.

Typically questions should include some kind of code but since I have a solution that works, I am including it as an answer so hopefully it will help a future reader.

Question:

Can you suggest a simpler solution of how to navigate a view based tableview based on the above parameters.

1
Is "Can you suggest a simpler solution of how to navigate a view based tableview based on the above parameters." still a question or is it a summary?Willeke
@Willeke Thanks for the comment. It's both actually. It took a considerable amount of research and time to craft a working solution so I thought I would share it with others but there could certainly be options that may provide shorter or cleaner code to achieve the same results. So it is a question. I try to keep on topic and follow the guide when answering or asking here on SO, so if it's not beneficial to others I can delete.Jay

1 Answers

1
votes

First is to subClass a view based NSTableView set set it up like this

class ItemTableView: NSTableView, NSTableViewDataSource, NSTableViewDelegate, NSTextFieldDelegate {

I have a tableView dataSource called transactionArray which holds the items to be displayed within the tableView. I also include the delegate methods

func numberOfRows(in tableView: NSTableView) -> Int

and

func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?

within the class so it's self contained.

Here's the function to handle tab navigation

// handle tab and backtab in an editable view based tableView subclass
// will also scroll the edited cell into view when tabbing into view that are outside the viewable area
//ref https://developer.apple.com/documentation/appkit/nscontroltexteditingdelegate/1428898-control
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
    print(#function)

    //let whichControl = control //this is the tableView textField where the event
                                 //  happened. In this case it will only be the
                                 //  NSTableCellView located within this tableView
    let whichSelector = commandSelector //this is the event; return, tab etc

    //these are the  keypresses we are interested in, tab, backtab, return/enter.
    let tabSelector = #selector( insertTab(_:) )
    //let returnSelector = #selector( insertNewline(_:) ) //use this if you need 
                                                          //custom return/enter handling
    let backtabSelector = #selector( insertBacktab(_:) )

    //if the user hits tab, need to determine where they are. If it's in the last
    //  column, need to see if there is another row and if so, move to next
    //  row, col 0 and go into edit. If it's a backtab in the first column, need
    //  to wrap back to the last row, last col and edit
    if whichSelector == tabSelector {
        let row = self.row(for: textView)
        let col = self.column(for: textView)
        let lastCol = self.tableColumns.count - 1

        if col == lastCol { //we tabbed forward in the last column
            let lastRow = self.transactionArray.count - 1
            var rowToEdit: Int!

            if row < lastRow { //if we are above the last row, go to the next row
                rowToEdit = row + 1

            } else { //if we are at the last row, last col, tab around to the first row, first col
                rowToEdit = 0
            }

            self.editColumn(0, row: rowToEdit, with: nil, select: true)
            self.scrollRowToVisible(rowToEdit)
            return true //tell the OS we handled the key binding
        } else {
            self.scrollColumnToVisible(col + 1)
        }

    } else if whichSelector == backtabSelector {
        let row = self.row(for: textView)
        let col = self.column(for: textView)

        if col == 0 { //we tabbed backward in the first column
            let lastCol = self.tableColumns.count - 1
            var rowToEdit: Int!

            if row > 0 { //and we are after row zero, back up a row and edit the last col
                rowToEdit = row - 1

            } else { // we are in row 0, col 0 so wrap forward to the last col, last row
                rowToEdit = self.transactionArray.count - 1
            }

            self.editColumn(lastCol, row: rowToEdit, with: nil, select: true)
            self.scrollRowToVisible(rowToEdit)
            self.scrollColumnToVisible(lastCol)
            return true
        }  else {
            self.scrollColumnToVisible(col - 1)
        }
    }

    return false //let the OS handle the key binding
}