NSSplitView Collapse Event - cocoa

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.

Related

Receive trigger when active NSView focus changes

I'm surprised that I haven't been able to find an answer for this from searching. So if there is a page describing how to do this, let me know, but I've been unable to find it.
I have 3 sibling NSOutlineViews all inside an NSSplitView I have added observers for NSOutlineViewSelectionDidChange which, when triggered update an NSTextView editor. I even test for negative row values to indicate that the user has unselected the row and clear the text.
The NSOutlineViews are connected to custom datasource objects and the NSSplitView is created by my custom NSWindowController in turn created by my custom NSDocument and custom NSDocumentController (I'm not using .nibs)
However I cannot seem to receive triggers for the NSOutlineViews changing their active status, it works if the user selects a different row in a different NSOutlineView, as the selection has changed but if they click the selected row of a different view, I don't receive any event or notification that anything has changed. Visually I can see the change as the row selection highlight colour changes from coloured to grey, in the outline view that has lost focus and the selection row colour changes from grey to coloured in the newly activated view.
I've tried to catch mouseDown events, tried becoming first responder, tried observing changes in the NSSplitView I've been right through the NSObject hierarchy from NSOutlineView to NSResponder looking for the appropriate notification or method. I found deprecated documentation regarding a focus change notification. I've tried combinations of nsview, nsnotification, nsoutlineview and various actions in google but can't find the 'this is how you do it'
EDIT:
this is the code I've added to my NSOutlineView subclass (along with prototypes in the headers) to become first responder, but it is never triggered.
- (BOOL)acceptsFirstResponder { return YES; }
- (BOOL)becomeFirstResponder {
NSLog(#"becomefirstResponder %#",self);
return YES;
}

NSWindow launching with wrong size after setting contentViewController to NSTabViewController

I have an Xcode project with an NSWindowController whose contentViewController was set to a subclass of NSViewController. I recently removed the NSViewController subclass from the storyboard and replaced the contentViewController with an NSTabViewController subclass.
Now, when I run the application, the NSWindow opens with a size of 500x500 instead of the size of the first tab. What's more, there is no view I can see in the storyboard that has a size of 500x500, and that size isn't being programmatically, either. The window itself is set to a different size, as is the view in the NSTabViewController's first NSViewController.
I'm assuming that there is some sort of constraint I have to set somewhere, but if there is, I don't know where/how to find it. Using Xcode 9.2 and High Sierra.
Programmatically setting the window's size to the correct size in windowDidLoad() works, but if I ever change the size of the view, I'll have to change that, as well, which will get old, quick.
Sorry if this is vague; I genuinely have no clue what kind of screenshot or code snippet would be helpful.
I recently ran into this frustrating problem as well.
There are a couple options to workaround this problem:
As you mentioned, set preferredContentSize in each of your custom view controllers that hold the tab's content to your desired size. This is inflexible but it does work.
// Swift
class FooViewController: ViewController {
override func viewWillAppear() {
super.viewWillAppear()
preferredContentSize = NSSize(width: 400, height: 280)
}
}
I found a hint to a better solution in this SO answer. You can add a subview (stackview, nsview, etc...) to the main view of the view controller that handles the tab's content (phew!) and then add constraints that pin it to each edge and add constraints that set the size.
Here's a screenshot of what it looks like in Interface Builder. I added a Stack View and then added 6 constraints.
Hope this helps.
Joshua's answer with setting the preferredContentSize did the trick, all kudos to him! One remark worth making is that since this is done exclusively for the parent tab view controller it's a good idea to subclass it and move this handling into tabView(_ tabView: NSTabView, didSelect tabViewItem: NSTabViewItem?) delegate method, which gets invoked when the tab is selected:
override func tabView(_ tabView: NSTabView, didSelect tabViewItem: NSTabViewItem?) {
tabViewItem?.viewController?.preferredContentSize = tabViewItem?.view?.frame.size
// Alternatively: tabViewItem?.viewController?.preferredContentSize = tabViewItem?.view?.fittingSize
super.tabView(tabView, didSelect: tabViewItem)
}
This way the preferred content size is always up to date and you can worry not about manually refreshing it, assuming the view provides the correct frame size or fitting size, which is easily achieved with constraints.
This method also get's invoked after the window controller finishes loading and where the 500×500 gets initially set.
Setting the preferred content size in every tabbed view controller itself is not ideal: the same code is duplicated across multiple controllers and adds unnecessary noise if these controllers are reused else where.
I had a similar issue. I added a view controller with a container view as the window content and pointed the container view content to the tab view controller.

Incorrect NSCursor originating from background NSView

I wanted to change the cursor in my cocoa application. I've followed the example in this answer, with success.
I have an NSView, let's call it NSViewA, behind another NSView, NSViewB. NSViewA contains a subclassed NSButton where the cursor has been changed. NSViewA and NSViewB have the same superview. So something like this:
- NSWindow
- NSViewA
- NSButtonSubclass
- NSViewB
My problem is that when NSViewB is shown, and the cursor is ontop of NSViewB, but in the same x y coordinates of the NSButton behind NSViewB, the cursor changes to that specified in the NSButton subclass.
How do I stop this behaviour?
The reason for this layout is that I'm creating a 'lightbox' control. If you think something along the lines of the NSWindow greying out, and a centred box appearing showing an error message. That sort of thing.
I previously had a problem where you could still click buttons, etc, behind NSViewB. This was solved by suppressing mouseDown: and mouseUp:. I've tried doing something similar with other mouse-related events, such as mouseEntered: and mouseExited: with no luck.
Could you make the addition of your custom cursor rectangle contingent on the enabled status of your button? In other words, your resetCursorRects would look like this:
// MyButton.swift
override func resetCursorRects() {
if enabled {
addCursorRect(bounds, cursor: NSCursor.pointingHandCursor())
}
}
Whenever viewB is about to be shown, disable the button, and call for the rects belonging to your button to be invalidated. If you're using Swift, you could do this second bit in a property observer attached to the enabled property itself:
// MyButton.swift
override var enabled: Bool {
didSet {
window!.invalidateCursorRectsForView(self)
}
}
If you don't want your button to take on a disabled look, make the addCursorRect call contingent on some other flag.

Cocoa: Avoiding 'Updates Continuously' in control binds

I have several panels that contain NSTextField controls bound to properties within the File's Owner object. If the user edits a field and then presses Tab, to move to the next field, it works as expected. However if the user doesn't press Tab and just presses the OK button, the new value is not set in the File's Owner object.
In order to workaround this I have set Updates Continuously in the binding, but this must be expensive (EDIT: or at least it's inelegant).
Is there a way to force the bind update when the OK button is pressed rather than using Updates Continuously?
You're right that you don't need to use the continuously updates value option.
If you're using bindings (which you are), then what you should be doing is calling the -commitEditing method of the NSController subclass that's managing the binding. You'd normally do this in your method that closes the sheet that you're displaying.
-commitEditing tells the controller to finish editing in the active control and commit the current edits to the bound object.
It's a good idea to call this whenever you are performing a persistence operation such as a save.
The solution to this is to 'end editing' in the action method that gets called by the OK button. As the pane is a subclass of NSWindowController, the NSWindow is easily accessible, however in your code you might have to get the NSWindow via a control you have bound to the controller; for example NSWindow *window = [_someControl window].
Below is the implementation of my okPressed action method.
In summary I believe this is a better solution to setting Updated Continuously in the bound controls.
- (IBAction)okPressed:(id)sender
{
NSWindow *window = [self window];
BOOL editingEnded = [window makeFirstResponder:window];
if (!editingEnded)
{
logwrn(#"Unable to end editing");
return;
}
if (_delegateRespondsToEditComplete)
{
[_delegate detailsEditComplete:&_mydetails];
}
}
Although this is really old, I absolutely disagree with the assumption that this question is based on.
Countinously updating the binding is absolutely not expensive. I guess you might think this updates the value continuously, understanding as "regularly based on some interval".
But this is not true. This just means it updates whenever you change the bound value. This means, when you type something in a textView, it would update as you write; this is what you'd want in this situation.

Hide child controls in an NSView

I have an NSView with multiple child controls in it. I know I can call [childControl setHidden:TRUE] but I was wondering if its possible to block the message "drawRect:" for the child controls.
Ive noticed that not calling [super drawRect:NSZeroRect] on the NSView does not affect the child controls. So my question is who calls the child controls drawRect message? And if there is a way to block it.
Thanks, Jose.
Every time the controls should react optically, they draw its view again.
If you really want to solve this problem like that, you can create for each control a subclass and add a code like this:
-(void)drawRect:(NSRect)rect {
if (!self.blocked) {
[super drawRect:rect];
}
}
The property "blocked" is a boolean which you have to set to YES or NO if you want to block it.
Note: To totally hide it the control subclass has to be blocked before it draws it self the first time.

Resources