tableViewSelectionDidChange with addSubview and removeFromSuperview in table row - swift

I looked for posts for issues with all 3 functions listed in the title, but my problem is not discussed anywhere. Frankly, my problem is such that it defies logic IMHO.
On selection of a row in my view-based NSTableView, I want to show a "Save" button in my last (aka "Action") column. So on viewDidLoad() I created my orphan button. Then on tableViewSelectionDidChange() I remove it from the superview and add it to my column's NSTableCellView. When there's no row selected, there is no button to show. Simple. Below code works perfectly.
func tableViewSelectionDidChange(_ notification: Notification) {
let selectedRow = tableView.selectedRow
btnSave!.removeFromSuperview()
if selectedRow > -1,
let rowView = rowView(selectedRow),
let actionView = rowView.view(atColumn: Column.action.hashValue) as? NSTableCellView {
actionView.addSubview(btnSave!)
}
}
Wait. Did I say Perfect? It works as long as I use mouse to change the selected row, including selecting below the last row in table which removes selection and any button in previous selected row. However, when I use the keyboard Up/Down keys to change the selection, it does not work.
First I thought the function will get called only if I use mouse to change row selection. However, that's not true. That's true for tableViewSelectionIsChanging as per docs and as per facts. But for tableViewSelectionDidChange docs don't say it will only work when mouse is used and the facts bear that out. I put print statements and function does get called. I stepped through debugger as well. The mind boggling part is - the method to remove button from superview works, but the one to add button as subview does not work - and only if I use keyboard. How is it possible that the same exact code executes but I get two different outcomes?
Adding remaining functions
I use this to get selected row
private func rowView(_ rowIndex: Int, _ make: Bool = false) -> NSTableRowView? {
return tableView.rowView(atRow: rowIndex, makeIfNecessary: make)
}
I call this in viewDidLoad to create my orphan button
private func createButton() {
btnSave = NSButton(frame: NSRect(x: 10, y: 0, width: 22, height: 16))
btnSave?.title = "S"
btnSave?.setButtonType(.momentaryPushIn)
btnSave?.bezelStyle = .roundRect
}

Related

How to move cursor when new NSTextView becomes FirstResponder (MacOS)?

Each time the user presses enter, a new NSTextView is made. This works correctly, and the new NSTextView becomes the first responder, as is my goal. However, the cursor does not move into the new NSTextView despite it being the first responder.
Below is my code:
func makeNSView(context: NSViewRepresentableContext<TextView>) -> TextView.NSViewType {
let textView = NSTextView()
textView.textContainer?.lineFragmentPadding = 10
textView.textContainerInset = .zero
textView.font = NSFont.systemFont(ofSize: 12)
textView.becomeFirstResponder()
//NSApp.activate(ignoringOtherApps: true) ----> doesn't make difference but is supposed to help switch cursor
print("\(textView) is first responder") //proof that the first responder is shifting, but the cursor does not move with it for some reason
return textView
}
I have tried to use this line as suggested by a different answer:
NSApp.activate(ignoringOtherApps: true)
However, it doesn't make any difference. I have also tried inserting text at a selected range, but it doesn't insert text in any of the NSTextViews displayed regardless of whether they are the first responder.
What methods exist to move the cursor to a different NSTextView (preferably to the First Responder)?
Let me know if any more information is needed.
I was able to get the desired behaviour by doing this:
func makeNSView(context: NSViewRepresentableContext<TextView>) -> TextView.NSViewType {
let textView = NSTextView()
textView.textContainer?.lineFragmentPadding = 10
textView.textContainerInset = .zero
textView.font = NSFont.systemFont(ofSize: 12)
DispatchQueue.main.async {textView.window?.makeFirstResponder(textView)
textView.setSelectedRange(NSMakeRange(0, 0)) }
print("\(textView) is first responder") //proof that the first responder is shifting, but the cursor does not for some reason
return textView
}
The answer originally comes from here: How to make first responder in SwiftUI macOS
Thanks for all the help!
As Warren Burton mentions in a comment, you have not added the new view to a window's view hierarchy at the point that you're trying to make it the first responder. That can't work.
Furthermore, calling becomeFirstResponder() does not make the receiver the first responder. (This is different from UIKit.) In fact, you're never supposed to call becomeFirstResponder() on macOS (except to forward to the superclass in an override). The documentation calls this out specifically:
Use the NSWindow makeFirstResponder(_:) method, not this method, to make an object the first responder. Never invoke this method directly.
(Emphasis added.) becomeFirstResponder() is called by Cocoa to inform a view (or other responder) that it has become the first responder. It doesn't cause a change, it notifies about a change.
So, once the view has been added to a window's view hierarchy, invoke textView.window.makeFirstResponder(textView).

How to trigger action when textfield becomes active

I have a CollectionViewController made up of custom cells. At this point my custom cells are only made up of UITextFields. When I click inside one of the textFields to begin typing I want to trigger an animation on the cell. For some reason I can't figure out why my animation isn't being triggered when I click inside the textField. When I conform to the UItextFieldDelegate protocol and try to trigger the action through the DidBeginEditing method it doesn't work. When I try to trigger the animation through UIControlEvents.touchDown it isn't working either.
#objc func animateCell(textField: UITextField) {
print("TextField active")
let cell = collectionView.cellForItem(at: indexPath)
UIView.animate(withDuration: 0.5, delay: 0, options: .allowAnimatedContent, animations: ({
cell?.frame = collectionView.bounds
collectionView.isScrollEnabled = false
}), completion: nil)
}
Instead of textFieldDidBeginEditing, try using textFieldShouldBeginEditing: (and make sure to return true from that method since you want to allow the user to type :-).
Also, since you say the method doesn't fire until you click on a different textfield outside of the first one selected, fire the animation manually for the initially selected text field as the collection view is displayed.

make NSTableView row stay selected Swift

how would I go about making a previously selectedRow of an NSTableView stay selected after the user presses a button calling a method to work with that selectedRow?
It worked in a previous programm looking like this:
selecting it gives it the OSX selection color, clicking the save button makes the textField becomeFirstResponder() which makes the row go grey automatically and doesnt change the selectedRow property. This is not done by my code, so im happy it just works.
Now in a similar program I want to achieve exactly this, but after clicking a button (calling a method making the textField becomeFirstResponder()) the visual selection is gone and also the tableView property selectedRow goes back to -1 (no row selected).
I tried a lot of ideas, most answers online are objective-C. Here is my most recent code (beeing called by the same button and pretty much faking what worked automatically before):
the NSTableViewAction method and the selectedRowCache variable:
var selectedRowCache = -1
#IBAction func alarmDataTableView(_ sender: NSTableView) {
selectedRowCache = alarmDataTableView.selectedRow
}
the buttonAction:
#IBAction func editAlarmButton(_ sender: NSButton) {
let row = alarmDataTableView.rowView(atRow: self.selectedRowCache, makeIfNecessary: false)
row?.backgroundColor = .blue
}
the selectedRowCache is just a variable that saves the selectedRow whenever the user selects a row (i dont want this).
no with this code the background color of the row doesnt do a thing (besides losing its selection after the button method is called).
the NSTableView is view based.
Thank you for any help!

Use first column for drag image in NSTableView

I'm implementing drag and drop in a table view, and the problem I'm having is that the drag image is always based on the column where the drag started, but I want to use the image from the first column.
I looked at Apple's TableViewPlayground sample, where the "Complex OutlineView Window" does what I want, but I can't figure out what I'm doing different in terms of setting up the drag.
I put together a simple example at https://github.com/Uncommon/TableTest - if you drag from the second column where it says "stuff", then it looks like you're dragging the word "stuff". What I want is to have the drag image always come from the first column (with the icon and name) regardless of which column you drag from.
In my data source I implement tableView(tableView:,pasteboardWriterForRow:) to enable dragging, but that doesn't include information about what column the drag started in. What am I missing from the example app to make this work?
The accepted answer didn't work for me. Instead, I had to implement the following optional function in NSTableViewDataSource:
func tableView(_ tableView: NSTableView, draggingSession session: NSDraggingSession, willBeginAt screenPoint: NSPoint, forRowIndexes rowIndexes: IndexSet) {
// Or whatever cell you want as the drag image
let cell = tableView.view(atColumn: 0, row: rowIndexes.first!, makeIfNecessary: false) as? NSTableCellView
session.enumerateDraggingItems(options: .concurrent, for: nil, classes: [NSPasteboardItem.self], searchOptions: [:]) {(draggingItem, idx, stop) in
if let cell = cell {
let rect = cell.bounds
let drag = session.draggingLocation
// You can customize the image starting point and size here
draggingItem.setDraggingFrame(NSRect(x: drag.x, y: drag.y, width: rect.width, height: rect.height), contents: cell.draggingImageComponents)
draggingItem.imageComponentsProvider = {
return cell.draggingImageComponents
}
}
}
}
Credit goes to this answer for providing much of this solution.
NSTableView drags the column where the drag started, NSOutlineView drags the outline column. If you want to change the image, subclass NSTableView and override dragImageForRows(with:tableColumns:event:offset:).
In response to why
dragImageForRows(with:tableColumns:event:offset:)
didn't work:
From the Apple documentation:
Note:
To support multi-item drags, it is highly recommended to implement the delegate method tableView:pasteboardWriterForRow: instead. Using that method will cause this method to not be called.

Custom `NSOutlineView` doesn't show context menu when right clicked

I'm implementing the rename/editing function in an NSOutlineView. The basic implementation is like:
#objc func renameAction() {
let row = outlineView.clickedRow
let rowView = outlineView.view(
atColumn: 0,
row: row,
makeIfNecessary: false) as! NSTableCellView
rowView.textField!.isEditable = true
rowView.window?.makeFirstResponder(rowView.textField!)
}
A mouseDown: is handled in the NSOutlineView so that the NSTextField can quit editing mode when clicking on the row that is being edited.
The idea of using a custom delegate is from this question
override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event)
let localLocation = self.convert(
event.locationInWindow,
from: nil)
let clickedRow = self.row(at: localLocation)
#if DEBUG
print(#file, #line, clickedRow)
#endif
if clickedRow != -1 {
self.extendedDelegate?.didClickRow(clickedRow)
}
}
In the delegate:
func didClickRow(_ row: Int) {
//... get the textField
textField.isEditable = false
textField.resignFirstResponder()
textField.window?.makeFirstResponder(
textField.window?.contentView
)
}
The strange thing is I can right click on an NSTableCellView(including both an icon and an NSTextField) to get the context menu before a left click on ANY row. After the left click, I can only get the context menu by right clicking anywhere outside the NSTableCellView (of any row, no matter it's the clicked one or not) but inside the row (the blank areas surrouding the NSTableCellView).
Screenshots
Before any left clicking, right clicking on the NSTableCellView gets the menu:
After a left clicking on any row, right click doesn't work "on" all the NSTableCellViews, but still works on the blank area of the row.
// No menu upon clicking on the NSTableCellView. But the menu will appear when clicking on the blank areas of the row.
Update:
I found that if the overriding mouseDown: is commented out, (as expected) there's no issue. But that will throw the ability to end editing by clicking on the current row.