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

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.

Related

How to make a NSTextView, that is added programmatically, active?

I am making an app where a user can click anywhere on the window and a NSTextView is added at the mouse location. I have got it working with the below code but I am not able to make it active (in focus) after adding it to the view (parent view). I have to click on the NSTextView to make it active but this is not what I want. I want it to automatically become active when its added to the parent view.
Code in my ViewController to add the NSTextView to its view:
private func addText(at point: NSPoint) {
let textView = MyTextView(frame: NSRect(origin: point, size: CGSize(width: 150.0, height: 40.0)))
view.addSubview(textView)
}
MyTextView class looks like below:
class MyTextView: NSTextView {
override var shouldDrawInsertionPoint: Bool {
true
}
override var canBecomeKeyView: Bool {
true
}
override func viewWillDraw() {
isHorizontallyResizable = true
isVerticallyResizable = true
insertionPointColor = .red
drawsBackground = false
isRichText = false
allowsUndo = true
font = NSFont.systemFont(ofSize: 40.0)
}
}
Also, I want it to lose focus (become inactive) when some other elements (view) are clicked. Right now, once a NSTextView becomes active, it stays active no matter what other elements I click except when I click on an empty space to create yet another NSTextView.
I have gone through the Apple docs multiple times but I think I am missing something. Any help would be much appreciated.
Get the NSWindow instance of the NSViewController's view and call makeFirstResponder passing the text view as parameter.
To lose focus call makeFirstResponder passing nil.

addCursorRect fails on NSButton after NSPopover loses focus

I have a NSPopover that contains two buttons. When I open the popover, the following code works to change the cursor to a pointing hand when hovering over the buttons and on clicking the button, 'Button pressed' appears in the console and an NSColorPanel appears as is desired.
class Button: NSButton {
override func resetCursorRects() {
super.resetCursorRects()
addCursorRect(bounds, cursor: .pointingHand)
}
}
#IBAction func buttonTapped(action:Any) {
print("Button pressed")
let cp = NSColorPanel.shared
cp.setTarget(self)
NSColorPanel.setPickerMode(.none)
cp.setAction(#selector(colorDidChange))
cp.isContinuous = false
cp.level = NSWindow.Level.statusBar
cp.makeKeyAndOrderFront(self)
}
However if I click anywhere else on the screen, and then go back to the NSPopover, the pointing hand cursor no longer appears when hovering over the button, and while the onClick event is still fired (as evidenced by 'Button pressed' logged in the console), the NSColorPanel doesn't open.
Any suggestions as to why this might be the case?
I ran into this issue and what solved it for me was adding the below code to the popover view controller viewWillAppear function.
NSApplication.shared.activate(ignoringOtherApps: true)

Custom Carbon key event handler fails after mouse events

I'm trying to write a custom NSMenu which will be able to list for key input and intercept the necessary events. This is to provide a simple search-as-you-type functionality for my open source clipboard manager.
It seems like the only way to do this is to install a custom Carbon event handler which will listen for key events and handler them accordingly, but it seems like there is an issue with such a custom handler.
Normally, I can propagate events downwards to other handlers (e.g. system ones) and they should be gracefully handled. This can be done by a simple callback:
let eventHandlerCallback: EventHandlerUPP = { eventHandlerCallRef, eventRef, userData in
let response = CallNextEventHandler(eventHandlerCallRef, eventRef!)
print("Response \(response)")
return response
}
This callback works perfectly and prints Response 0 all the time. This response means that the event is handled correctly.
However, things get weird once we send mouse events before keyboard events. In such case, the callback fails and prints Response -9874. This response means that the event was not handled correctly.
It seems like the event fails to be handled somewhere below my custom view and I don't know where exactly or how to overcome this issue.
To reproduce, I've uploaded the code to Gist which can be added to XCode playground and run. Once you see menu popup, press some keys (preferably arrow keys as they won't close the menu) and observe Response 0 in the console. After that, move cursor inside the menu and press more arrow keys. You should see Response -9874 in the console now.
Unclear if you have an NSTextField as your menu view, but if you use one then it's easy to setup a delegate for that text field that can get the current contents of the field as the user types (this takes care of them moving backwards with the arrow keys and then deleting characters, using the delete key, etc). Your delegate implements the appropriate delegate method and gets called each time the text changes:
extension CustomMenuItemViewController: NSTextFieldDelegate {
func controlTextDidChange( _ obj: Notification) {
if let postingObject = obj.object as? NSTextField {
let text = postingObject.stringValue
print("the text is now: \(text)")
}
}
}
Just to confirm this works as expected, I created the ViewController class for the custom menu item views (label + edit field) in a xib file and then dynamically built a simple test menu with a single menu item that has the custom view controller's view and added it to the menubar inside my app delegate:
func installCustomMenuItem() {
let menuBarItem = NSMenuItem(title: "Test", action: nil, keyEquivalent: "")
let menu = NSMenu(title: "TestMenu" )
let subMenuBarItem = NSMenuItem(title: "Custom View", action: nil, keyEquivalent: "")
subMenuBarItem.view = menuItemVC.view
menu.addItem(subMenuBarItem)
menuBarItem.submenu = menu
NSApp.mainMenu?.addItem(menuBarItem)
}
Looks like this after I typed "hello":
And you can from the console that my handler got called for every character typed:
the text is now: H
the text is now: He
the text is now: Hel
the text is now: Hell
the text is now: Hello
Your situation is probably a little different, but it seems like this approach is very clean and might work for you. If it won't for some reason, add a clarifying comment and we'll see if we can't make it work for you.
Addition:
It occurred to me that you might wish to not use NSTextField and so I was curious if it was as easy to do this with a custom view and it's relatively easy.
Make a subclass of NSView:
class CustomMenuView: NSView {
override var acceptsFirstResponder: Bool {
return true
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
// Drawing code here.
}
override func keyDown(with event: NSEvent) {
print("key down with character: \(String(describing: event.characters)) " )
}
}
Set the class of your root view in the custom view controller to be of this type of class and then all done as before - view controller loaded in applicationDidFinishLaunching and menus built and view controller's view (which is now a CustomMenuView) is set as the menuBarItem.view.
That's it. You now get your keyDown method called for every key down when the menu is dropped down.
key down with character: Optional("H")
key down with character: Optional("e")
key down with character: Optional("l")
key down with character: Optional("l")
key down with character: Optional("o")
key down with character: Optional(" ")
key down with character: Optional("T")
key down with character: Optional("h")
key down with character: Optional("i")
key down with character: Optional("s")
key down with character: Optional(" ")
key down with character: Optional("i")
key down with character: Optional("s")
key down with character: Optional(" ")
key down with character: Optional("c")
key down with character: Optional("o")
key down with character: Optional("o")
key down with character: Optional("l")
:)
Now your custom view (and subviews if you like) can do their own drawing and so on.
Addition with requested sample without the ViewController:
// Simple swift playground test
// The pop-up menu will show up onscreen in the playground at a fixed location.
// Click in the popup and then all key commands will be logged.
// The ViewController in my example above may be taking care of putting the custom view in the responder chain, or the fact that it's in a menubar and being invoked via a MenuItem might be.
// I'd suggest trying it in the actual environment rather than in a playground. In my test app you click the menu name in the menubar to drop down the menu and it is added to the responder chain and works as expected without having to click in the menu first to get the events flowing.
// There is no reason you need to be hooking events either with carbon events or the newer format. If you're in the responder chain of and implement the necessary, method then you'll get the key events you're looking for.
import AppKit
class CustomMenuView: NSView {
override var acceptsFirstResponder: Bool {
return true
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
// Drawing code here.
}
override func keyDown(with event: NSEvent) {
print("key down with character: \(String(describing: event.characters)) " )
}
}
func installCustomMenuItem() -> NSMenu {
// let menuBarItem = NSMenuItem(title: "Test", action: nil, keyEquivalent: "")
let resultMenu = NSMenu(title: "TestMenu" )
let subMenuBarItem = NSMenuItem(title: "Custom View", action: nil, keyEquivalent: "")
subMenuBarItem.view = CustomMenuView(frame: NSRect(x: 0, y: 0, width: 40, height: 44))
resultMenu.addItem(subMenuBarItem)
// menuBarItem.submenu = menu
return resultMenu
}
var menu = installCustomMenuItem()
menu.popUp(positioning: nil, at: NSPoint(x:600,y:400), in: nil)
I didn't manage to figure out why this issue was happening or how to fix it, but I understood that it's possible to work around this issue by intercepting all the keys and simulating their behavior manually.
For example, this is how I now handle down arrow key which is supposed to select next item in menu list:
class Menu: NSMenu {
func selectNext() {
var indexToHighlight = 1
if let item = highlightedItem {
indexToHighlight = index(of: item) + 1
}
if let itemToHighlight = self.item(at: indexToHighlight) {
let highlightItemSelector = NSSelectorFromString("highlightItem:")
perform(highlightItemSelector, with: itemToHighlight)
if itemToHighlight.isSeparatorItem || !itemToHighlight.isEnabled || itemToHighlight.isHidden {
selectNext()
}
}
}
}
This way, when I receive a key down event with down arrow key - I can just call the function and return true to prevent the event from reaching default NSMenu handler. Similarly, up arrow key can be done.
In case of a return key, I ended up with the following code:
class Menu: NSMenu {
func select() {
if let item = highlightedItem {
performActionForItem(at: index(of: item))
cancelTracking()
}
}
}
The full commit implementing this is https://github.com/p0deje/Maccy/commit/158610d1d.

tableViewSelectionDidChange with addSubview and removeFromSuperview in table row

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
}

Select next NSTextField with Tab key in Swift

Is there a way to change the responder or select another textfield by pressing tab on the keyboard, in Swift?
Notes:
It's for a fill in the blank type application.
My VC creates a list of Words [Word], and each of those words has its own WordView - word.wordView. The WordView is what is displayed. WordView is a child of NSTextField.
I tried to override keydown but it doesn't allow me to type anything in the text view.
You have to connect your textField nextKeyView to the next textField through the IB or programmatically:
textField1.nextKeyView = textField2
Assuming you want to go from textView1 to textView2. First set the delegate:
self.textView1.delegate = self
Then implement the delegate method:
func textView(textView: NSTextView, doCommandBySelector commandSelector: Selector) -> Bool {
if commandSelector == "insertTab:" && textView == self.textView1 {
self.window.makeFirstResponder(self.textView2)
return true
}
return false
}
If you want some control over how your field tabs or moves with arrow keys between fields in Swift, you can add this to your delegate along with some move meaningful code to do the actual moving like move next by finding the control on the superview visibly displayed below or just to the right of the control and can accept focus.
public func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
switch commandSelector {
case #selector(NSResponder.insertTab(_:)), #selector(NSResponder.moveDown(_:)):
// Move to the next field
Swift.print("Move next")
return true
case #selector(NSResponder.moveUp(_:)):
// Move to the previous field
Swift.print("Move previous")
return true
default:
return false
}
return false // I didn't do anything
}
I had the same or a similar problem, in that I wanted to use an NSTextView field, to allow multiple lines of text to be entered, but it was the sort of field where entering a tab character would make no sense. I found an easy fix for this: NSTextView has an instance property of isFieldEditor, which is set to false by default; simply set this to true, and tabs will now skip to the next field.