Because I am sub-classing NSTextField, I believe the correct (or at least the working) answer is, as suggested, to override becomeFirstResponder:
with something along the lines of the following, where myDocView
is the scrolling content of the parent scrolling view somewhere up higher in the view hierarchy:
- (BOOL)becomeFirstResponder
{
BOOL done = [super becomeFirstResponder];
if (done) {
// Ensure new focus ring is shown also (shouldn't be hardwired, but for now ...)
NSSize margin = NSMakeSize(20.0, 20.0);
// Get where text field lives in myDocView's coordinates
NSRect r = [myDocView convertRect:[self bounds] fromView:self];
// Try scrolling if partially visible first, and if not ...
if (![myDocView adjustRectIntoView:r withMargin:margin]) {
// Text edit field is not visible in parent scroll view
// so tell document view where to scroll itself to
margin = NSMakeSize(-30.0, -30.0);
[myDocView specialScrollTo:self withOffset:margin];
}
}
return done;
}
The property field myDocView
belonging to the text edit field sub-class has to be set when the text editing object is programmatically created, so that the text editing field knows who to send the scrolling message to when it becomes first responder. This is because in my particular case the text editing sub-classed object is actually several view levels below that of the scrolling document view.
Partly for general user-interface reasons, and partly because of the particulars of how my scrolling content view is laid out, it is necessary to do something different in three cases. The first case is when the text editing field that is becoming the first responder is already fully visible. That's easy, adjustRectIntoView
does nothing but returns YES
, because the user can see the focus ring change.
In the second case, the text editing field is partially visible. In this case, the method adjustRectIntoView:withMargin:
makes it fully visible (if possible, otherwise, it makes the origin area visible). But it does so using only a minimal scrolling movement, either horizontally or vertically (and only both if necessary), leaving the field next to the nearest edge of the scrolling view's visible rect. This "startles" the user the least.
Finally, if the field was completely invisible, then in my particular case I have to do a special analysis of other nearby views related to the text editing view so as to bring it (or them all) into view for the user to see.
Both adjustRectIntoView:withMargin:
and specialScrollTo:withOffset:
are methods added to the (sub-classed) myDocView
that's being scrolled.
My latter special routine is too app-specific, but the former is pretty general and looks like this (and can be easily modified to accomplish the latter):
- (BOOL)adjustRectIntoView:(NSRect)r withMargin:(NSSize)margin
{
CGRectInset(r, -margin.width, -margin.height);
NSRect vis = [myScroller documentVisibleRect];
if (CGRectContainsRect(vis, r)) {
// The enhanced rectangle `r` is already fully visible,
// so we're done (no change)
return(YES);
}
if (!CGRectIntersectsRect(vis, r)) {
// The enhanced rectangle `r` is fully invisible, so caller
// must apply whatever other custom strategy it needs to
// scroll `r` into view; or don't return and fall through.
return(NO);
}
// Rectangle `r` is partly visible in scroll view. So nudge the
// scrolling view enough to bring `r` into view near where it
// already is, with a minimum of motion. If `r` contains `vis`,
// which can happen if `r` is part of a highly magnified view,
// this gives preference to `r`'s origin becoming visible.
NSPoint ul = r.origin;
NSPoint ur = NSMakePoint(r.origin.x+r.size.width, r.origin.y);
NSPoint ll = NSMakePoint(r.origin.x, r.origin.y+r.size.height);
NSSize amt;
if (ul.x < vis.origin.x)
amt.width = (ul.x - vis.origin.x);
else if (ur.x > vis.origin.x+vis.size.width)
amt.width = (ur.x - (vis.origin.x+vis.size.width));
else
amt.width = 0.0;
if (ul.y < vis.origin.y)
amt.height = (ul.y - vis.origin.y);
else if (ll.y > vis.origin.y+vis.size.height)
amt.height = (ll.y - (vis.origin.y+vis.size.height));
else
amt.height = 0.0;
vis.origin.x += amt.width;
vis.origin.y += amt.height;
[[myScroller documentView] scrollPoint:vis.origin];
return(YES);
}