Rich, Window-like Context Menu (like Interface Builder) - macos

I need a context menu that is similar in capabilities to what Interface Builder presents when right(control)-clicking on a view, view controller etc.:
At first sight, it looks like an NSPanel with its style attribute set to "HUD Panel", containing some sort of outline view.
The window itself shouldn't be difficult to implement, but the usual way of presenting a context menu on right(control)-click is by overriding the method:
func menu(for event: NSEvent) -> NSMenu?
...which takes an NSMenu as the return value; can't pass an NSWindow / NSPanel instead.
Perhaps I could do something like this:
override func menu(for event: NSEvent) -> NSMenu? {
// Create our popup window (HUD Panel) and present it
// at the location of event:
// (...actual code omitted...)
// Prevent any actual menu from being displayed:
return nil
}
...but it feels like a hack; I am tricking the system into giving away the timing of the right(control)-click event by pretending to care about presenting an actual NSMenu (i.e., overriding a method explicitly intended for that), but using that timing to do something different.
I would also need to place some logic to dismiss the window when the user clicks somewhere else (context menues have this functionality built in).
I don't think that subclassing NSMenu and NSMenuItem to obtain the above behaviour and appearance is feasible either...
Is there a better way?
Does anybody know (or is able to guess) what Interface Builder actually does?
EDIT: As pointed out in the comment by #Willeke, the conditions for the panel to be shown are not exactly the same as a context menu, so it most surely is not one. This means that hijacking the method above in order to display the window is not just unelegant, but wrong. The question stands as to How to Display the Window (i.e., detect static, non-dragging right click).

You'll want to override rightMouseDown(with:) from your view subclass and use that to trigger showing the panel. NSView's implementation of that method is what calls menu(for:) and presents the returned menu, so your custom subclass can use it to show its custom menu panel instead of calling super.
For full behavioral consistency with both standard context menus and the menus in Interface Builder, you'll also want to handle ctrl-left-clicks by overriding mouseDown(with:) and check for if the event's modifierFlags includes .control.

Related

NSSplitView Collapse Event

I'm trying to implement an NSSplitView similar to Xcode where you can collapse a view by dragging its handle to under half its width and it will collapse automatically. I have that part working, but I need to update the state of a button in the toolbar when this happens. I tried listening to splitViewDidResizeSubviews and checking if the splitView's view is collapsed, but that method fires 16 times with collapsed == true, so I don't think I want to update the button's state 16 times. Is there a cleaner way to do this? I'm new to Cocoa, but from what I've seen, I would expect there to be some way to just say bind this button's state to the isCollapsed property and be done with it. Does such a thing exist?
If you subclass your NSSplitViewController you can add a listener for the SplitViewItem's isCollapsed property:
class MySplitViewController: NSSplitViewController {
var observer: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
let sideViewSplitViewItem = splitViewItems[0]
observer = sideViewSplitViewItem.observe(\.isCollapsed, options: [.initial, .new]) {splitViewItem, _ in
print("Sidebar collapsed state changed to: \(splitViewItem.isCollapsed)")
}
}
}
The best way to see what bindings are available is to check the docs, specifically the Cocoa Bindings Reference (look in the sidebar for the view you're after).
NSSplitView doesn't have the binding you describe, so I think you're on the right track with your current approach. Of course, you don't need to update the button's state sixteen times, just check it's value each time, and update it if needs be. It might seem a bit wasteful, but checking the value of a bool is a very cheap operation, and you won't notice any kind of performance hit.
While NSSplitView has no event or observable property for when one of it's subviews is "collapsed", the immediate subview itself will have its hidden property set to YES.
So you can either observe the subview's hidden property yourself, or if you're creating your own custom subview of NSView, you can override the -(void) setHidden:(BOOL)hidden to catch the event.
I do the latter, and it works correctly every time.
-(void)setHidden:(BOOL)hidden
{
[super setHidden:hidden];
// Insert code to react to the "collapsed" event here.
// (You're likely going to tell your parent view,
// self.window.windowController, or something.)
}
Of course the setHidden method / hidden property can in theory be called for purposes other than from NSSplitView "collapsing" your view, but this is very unlikely to ever happen in practice.

How to show NSTextView-like context menu

I want to present NSTextView-like context menu for a certain NSString object (say, "text") each time -rightMouseDown-method of my custom NSResponder-subclass get called.
Where can I obtain these items for some NSString?
UPD
Created NSTextView instance and used its -menuForEvent:-method. Some of items was disabled (like 'copy' and so on), I reset its target value to custom object with overridden selectors (like -copy: and others).
NSTextView is an NSView subclass, which provides a class method +defaultMenu. So you can ask for NSMenu * menu = [NSTextView defaultMenu]; to get your very own copy to do with as you please. You may have to walk its structure and customize individual NSMenuItem instances to adjust their target/action but most (all?) should work just fine with nil-target (sends action to first responder) and their default action.
I must admit, however, I'm not sure what you mean by "Where can I obtain these items for some NSString?" The context menu is opened from some UI control (like a text view) and sends its action (like -checkSpelling...) to some target (like the first responder; which should be something like a text view, which acts as the view for a string or attributed string) to act upon.

Disabling the NSTableView row focus ring

How do you disable the focus ring around an NSTableView row when the user right-clicks on it? I can't get it to disappear. Setting focus ring of an individual NSTableViewCell in the table to None has no effect.
Subclass the table view and implement the following method:
- (void)drawContextMenuHighlightForRow:(NSInteger)row {
// do nothing
}
Note:
The blue outline is not the focus ring.
This is an undocumented private method Apple uses to draw the outline. Providing an empty implementation will prevent anything from being drawn, but I am not 100% sure that whether it can pass the review.
New:
Here is how I did it.
You can handle the menu manually. Subclass NSTableRowView or NSTableCellView, then use rightMouseDown: and mouseDown: (check for control key) and then notify your tableViewController (notification or delegate) of the click. Don't forget to pass the event as well, then you can display the menu with the event on the table view without the focus ring.
The above answer is easier, but it may not pass the review, as the author mentioned.
Plus you can show individual menu items for each row (if you have different sorts of views)
Old:
I think the focus ring is defined by NSTableRowView, not NSTableCellView, because it is responsible for the complete row. Try to change the focus ring there. You can subclass NSTableRowView and add it to the tableView via IB or NSTableViewDelegate's method:
- (NSTableRowView *)tableView:(NSTableView *)tableView rowViewForRow:(NSInteger)row
If your goal is just displaying a contextual menu, you also can try this.
Wrap the NSOutlineView within another NSView.
Override menuForEvent method on the wrapper view.
Do not set menu on outline-view.
Now the wrapper view shows the menu instead of your outline-view, so it won't show the focus ring. See How do you add context senstive menu to NSOutlineView (ie right click menu) for how to find a node at the menu event.

FirstResponder as delegate for NSToolBar

I have a Mac app that consists of a window with a variable number of panes in it, each containing a tableview. The window has a toolbar with buttons, and I want the VC for the currently selected pane to handle validating the toolbar items, as well as being target for their actions.
If I could set first responder as delegate for the toolbar, this would be handled automatically, so my question is if that is possible! I have obviously googled around for this and some articles seem to hint that it is possible, but IB doesn't seem to let me do it.
An NSWindowController subclass would be better suited for this, that is the toolbar's delegate (it's natural role anyway) and can talk with the currently selected pane, using a custom protocol to decide on business logic.
Same goes for the UI/Menu action handlers; the window controller is perfect for this and your design will fit within it well.
It's not really got anything to do with the first responder as you are interested in the currently selected pane, not the first responder.

Override key equivalent for NSButton in NSTextField

I have a window with two NSButtons and an NSTextField along with several views and several other controls. I assign the right and left arrows keys to each of the two NSButtons. The two buttons respond to the right and left arrow keys. However, when in the NSTextField, I want the arrow keys to perform as the normally would in a text field and not trigger the NSButtons. I have tried reading through the Cocoa Key Handling documentation and other questions regarding key events, but I could not find an example of trying change the key equivalent behavior in one control. I tried subclassing the NSTextField but couldn't trap the arrow keys. How can this be implemented?
You can override becomeFirstResponder: and in your implementation call setKeyEquivalent:. If you want to remove the key equivalent when the button loses first responder status, override resignFirstResponder:.
Do this in the control whose first-responder status you want to affect the button's equivalent. For example, if you have a view as a container and it can become first responder, you'd override -becomeFirstResponder: (calling super) then manage the button's equivalent there. If you don't yet understand these topics, you have a lot of prerequisite reading to do because a simple answer isn't possible here.
You could subclass NSButton and override performKeyEquivalent: like so:
override func performKeyEquivalent(event: NSEvent) -> Bool {
guard
let window = window where window.firstResponder.isKindOfClass(NSText.self) == false
else {
return false
}
return super.performKeyEquivalent(event)
}
This essentially disables the key equivalent if the first responder is a text field/view.

Resources