NSTextView overlay causes oddities with first responder status - cocoa

I have an NSTextView in an NSScrollView, and I am programmatically inserting an NSView subclass as a subview of the NSTextView. This NSView acts as an overlay, superimposing graphical information about the text beneath it.
I thought it was working quite well until I noticed that the text view does not respond to right clicks. Other operations (editing, selection) seem to work just fine.
Also, if the first responder is changed to a sibling of the scroll view (an outline view, for example) the text view does not regain first responder status from clicking on it. The selection will change in response to clicking, but the selection highlight is gray instead of blue (indicating that the text view is not the first responder).
If I offset the frame of the overlay subview, the text view behaves 100% normally in the area not overlapped by the overlay, but the overlapped area behaves incorrectly, as outlined above.
Steps To Replicate This Behavior on Mac OS X 10.6.4:
Create a plain old non-document-based Cocoa app.
Add an `NSTextView' IBOutlet to the app delegate .h.
Add an NSTextView to the window in MainMenu.xib. Connect the textView outlet.
Type in a bit of code:
In applicationDidFinishLaunching:
NSView *overlay = [[NSView alloc] initWithFrame:textView.bounds];
[textView addSubview:overlay];
[overlay release];
Run the app, observe that right click in the text area does not work as it should, yet you can still otherwise interact with the text view.
Next, add an NSOutlineView to the window in the xib. Observe that once focus leaves the text area (if you click on the outline view) with the overlay in place, you cannot set the focus back to the text view (it will not become first responder again).
Is there some way I can enable the NSTextView to receive all of its events, even though my NSView overlay does not accept first responder or mouse events? I suspect this might be related to the field editor – perhaps it is ignoring events it thinks are destined to the overlay view?

You probably need to make your overlay an instance of a custom view class that forwards all events and accessibility messages to the text view. You may also need to convert any view-relative coordinates to the text view's coordinate system.

I don't have a lot of experience with it, but another possibility would be to use a Core Animation layer as an overlay.

A clean way to handle this is by making your overlay view a custom subclass of NSView, and then overriding the hitTest: method to always return nil. This will prevent the overlay view from participating in the responder chain. Instead, events will get sent automatically to it's superview or views higher up the view hierarchy. You might also want to override acceptsFirstResponder to return NO to be safe (in case it's accidentally set programatically).

Related

How to call NSScrollView autoscroll-method programmatically

I have simple chat application with text messages view-based NSTableView as you can see at the picture below.
Each message contains NSTextView instance having height to fit all the text.
All I need is to start NSScrollView (which NSTableView-instance is enclosed by) autoscrolling while the user selecting text dragging mouse far enough. Unfortunately, autoscrolling doesn't appear. In case of dragging somewhere outside of the text views all succeed.
I tried to call autoscroll:-method directly by simply push NSEvent-instance from NSTextView-subclass "mouse dragged"-event (like in example from this article):
- (void)mouseDragged:(NSEvent *)event
{
[self.scrollView autoscroll:event];
}
As I've overrode all the mouse events and implemented all the text selecting, this method often invokes. But the autoscrolling doesn't seem to work.
UPDATE
I figured out that before calling -autoscroll:-method there must be -mouseDown: of the same object. But it breaks my text selecting mechanism. The point even not in being first responder, there must be nothing but the mouseDown:-method.
Normally, a text view is within a scroll view of its own. Even if that's big enough to show all of the text without scrolling, it's still there. A call of -autoscroll: on anything within that scroll view (possibly including that scroll view itself?) will just try to scroll that scroll view, not the scroll view that contains the table view.
Try calling -autoscroll: on a view higher up in the hierarchy. Either self.scrollView.superview, the table cell view, or the table view.
Note, though, that the table view's scroll view will keep scrolling even after the cell view containing the text view is fully on-screen. In fact, it may keep scrolling it so far that it's off the screen in the other direction. Basically, it doesn't know that you're trying to select within the text view so it doesn't know to stop when the selection extends all the way to the edge of the text view.
Another approach might be to try to use a "bare" text view with no enclosing scroll view. I don't think IB will let you do that, so you'd have to do it programmatically. Bare text views don't play well with auto layout, though.

How do I do something after the next NSView -layout has occurred?

In response to a user event, I want to:
add a new NSView to the window, and then
show an NSPanel positioned just below that view
I have each half of this done. I can add a new subview, and the container view's -updateConstraints identifies it and adds the correct layout constraints, so that the next time layout is performed, it's positioned correctly in the window. Also, I have a NSWindowController subclass that puts the panel on the screen.
Unfortunately, there's an ordering problem. My panel's controller just looks at the new NSView's frame property for deciding where to put it, but during this iteration of the main event loop, the -layout method hasn't been called yet, so it's still positioned at (0,0).
(If I separate these two pieces of functionality, and require two separate user events for "add view" and "create panel", then the panel is correctly positioned below the view.)
Is there a way to attach an NSPanel to an NSView, as if with a layout constraint? Or is there a way to say "do this (window controller stuff), but only after the next -layout call"?
Just call -layoutSubtreeIfNeeded on your NSView’s superview as soon as you add it and its constraints, so it will lay out immediately, then add the panel.
Or use an NSPopOver, although those draw a certain way and you might not want that.

How to place an NSButton over an NSTableView

1) No table behind button
2) Table loaded
3) After scrolling
If I place a button over an NSTableView I get artifacts being left behind after scrolling. Does anyone know how to fix this?
My current solution is just to split the section with the table into 2. The lower portion is a disabled button in the background.
Try the NSScrollView method
- (void)addFloatingSubview:(NSView *)view forAxis:(NSEventGestureAxis)axis
All tableviews should normally be inside a ScrollView.
I had a similar problem and solved it, see Why does an NSTextField leave artifacts over an NSTableView when the table scrolls?. Essentially OSX will draw the contents of the table and let the parent scroll view move the cached contents avoiding redraws to be performant. By subclassing the parent scroll view it can be forced to refresh the table hooking the reflectScrolledClipView: method. Then the whole table, including overlays, will be redrawn with each scroll.
That's because the scroll view is set to copy its contents when scrolling and only redraw the newly-uncovered part as a performance optimization. To turn that off, use
myTableView.enclosingScrollView.contentView.copiesOnScroll = NO;
though that will make scrolling use more CPU (you can also do this in the XIB, look for a 'copies on scroll' checkbox).
Probably a better approach would be to switch the scroll view and the button to be layer-backed:
myTableView.enclosingScrollView.wantsLayer = YES;
myButtonView.wantsLayer = YES;
(Again, you can set this in the 'Layers' inspector of the XIB file, where you can click a checkbox next to each view to give it a layer) Now, the scroll view will only copy stuff from its own layer (which no longer includes your button). Also, now all the compositing of the text view will be done using in the graphics card. This works fine with an opaque push button, however, if you put text on a transparent background in its own layer (e.g. if you have a transparent pushbutton with a title), you lose sub-pixel anti-aliasing.

NSScrollView and ScrollToPoint on an NSImage

I have a NSView as the documentView for a NSScrollView. I also have a NSImageView as a subview of the NSView. The image dynamically changes size so the scroll bars become active/inactive at various times. Once the image has changed, I'd like to scroll to a certain point on the image. From within the NSView's drawRect: method, I call
[[myScrollView contentView] scrollToPoint: myPoint];
The scroll bars update and the image appears as I'd like, but as soon as the image is scrolled, a double image appears or parts of the image get cut off. Any help would be appreciated. Thank you.
Sounds like you might want to turn off the "Copy On Scroll" behavior option of the NSScrollView either in Interface Builder or programmatically.
From Scroll View Programming Guide for Mac OS X: How Scrolling Works:
The NSClipView class provides low-level scrolling support through the
scrollToPoint: method. This method translates the origin of the
content view’s bounds rectangle and optimizes redisplay by copying as
much of the rendered document view as remains visible, only asking the
document view to draw newly exposed regions. This usually improves
scrolling performance but may not always be appropriate. You can turn
this behavior off using the NSClipView method setCopiesOnScroll:
passing NO as the parameter. If you do leave copy-on-scroll active, be
sure to scroll the document view programmatically using the NSView
method scrollPoint: method rather than translateOriginToPoint:.

NSTextField over NSOpenGLView

I have made a window with an NSOpenGLView that I am rendering openGL content into.
I want to add some buttons and text fields to the view: I can add NSTextFields and NSButtons using interface builder (or code) but they do not appear.
NSOpenGLView is documented as not being able to have sub views, so I then made my own CustomGLView by deriving directly from NSView and implementing the code to create and use a NSOpenGLContext in it. But the subviews are still not appearing :- the OpenGL context paints over them.
On Windows this problem does not exist:- Windows used to host OpenGL MUST have the WS_CLIPCHILDREN and WS_CHIPSIBLINGS styles set ensuring that any peer, or sub children (views) will not be obscured by the OpenGL surface.
How do I get subviews to display over a NSView thats drawing using OpenGL ?
You have 2 choices:
Create a window just for the text field. Add as a child window of the one hosting the OpenGL view. Major downside is you have to manage positioning it correctly if the Open GL view is moved.
Set up your view hierarchy like so:
Layer-backed view
Layer-hosting view whose layer contains an OpenGL layer
Text field
Simply call -setWantsLayer:YES on the subviews of the NSOpenGLView.
NSOpenGLView cannot have subviews according to the documentation. Even if you subclass the NSOpenGLView, that will change nothing.
What you can do is to create a NSView that will hold both the NSOpenGLView and the NSTextField. You then overlap them in the right order to make one draw atop the other.
I'm not heavily into OpenGL yet, but it's my understanding that you can accomplish the visual effect of subviews with Quartz Extreme using layer-backed views; however, those may be problematic. Since subviews are not supported directly, any solution is liable to be a hack.
Indeed, the solution in that link actually hacks a second window to appear over your OpenGL display, the second window displaying the Cocoa views you desire.
The following code (from the above link) is something I've not tested (again not being an OpenGL guy by nature -- yet), but appears like a fairly clever approach:
// This is the GL Window and view you already have
glWindow = [[GLWindow alloc] initWithContentRect:windowRect];
glView = [[[GLView alloc] initWithFrame:NSMakeRect(0, 0, windowRect.size.width, windowRect.size.height)] autorelease];
[glView translateOriginToPoint:NSMakePoint(glView.bounds.size.width/2, glView.bounds.size.height/2)];
[glWindow setContentView:glView];
// And here's your transparent UI window
uiWindow = [[TransparentWindow alloc] initWithContentRect:windowRect];
uiView = [[[NSView alloc] initWithFrame:NSMakeRect(0, 0, windowRect.size.width, windowRect.size.height)] autorelease];
[uiView translateOriginToPoint:NSMakePoint(uiView.bounds.size.width/2, uiView.bounds.size.height/2)];
uiView.wantsLayer = YES;
[uiWindow setContentView:uiView];
[glWindow addChildWindow:uiWindow ordered:NSWindowAbove];
Again, I've not tested this, but it looks like it will get you the visual effect you desire.
The text can be rendered into a texture -- I just used this for a project, did a lot of looking for sample code, and ultimately found Apple's GLString demo code, which was an absolute trove of how-to:
http://developer.apple.com/library/mac/#samplecode/CocoaGL/Listings/GLString_m.html
I haven't tried adding buttons, but you can, of course, draw your own and comparing the positions of click events with those of your buttons...
This was my solution:
1) Create a parent NSView (let's call it parentView).
2) Add an NSOpenGLView Child to parentView.
3) Add an additional NSView Child to parentView (make sure this is after the OpenGLView within the hierarchy). You can add additional TextFields, etc. to this view.
4) In the ViewController for the parent make sure you call [parentView setWantsLayer: TRUE]; I did this within -(void) viewWillAppear
1) The NSOpenGLView can have a subview. It can have plenty even.
2) The reason some views, controls and other elements are being bullied by NSOpenGLView is due to the loading process when the Application launches. I.e If you add a slider or textfield above and into the content view of the window where the NSOpenGLView also resides, upon Application-Launch that textfield will most likely wind up beneath the NSOpenGLView.
This is an Apple Bug. And they know about it.
You can solve it quite easily even without adding a subview to NSOpenGLView...
In Interface Builder drag i.e. a CustomView into the canvas (Not the view). And set it the way you want it with sliders, text and what not. Then create an outlet (Call it i.e topView) in your view controller. Then somewhere in your code... Perhaps (applicationDidFinishLaunching) add this line...
[_window.contentView addSubview:_topView];
(Do your positioning & layout)
This will do the exact same thing as if you had dragged it into the contentView yourself inside IB. Only it will draw the darn thing in the correct Z position.
You loose IB's constraints this way and have to it manually
One could also just subclass and CAOpenGLLayer and use that as a backing layer inside of a regular NSView. There too it is drawn correctly...
Here is Apple's way of wanting to do that. CALayers are a Godsend ;)
Enter following String ** NSOpenGLLayer** in search and hit enter to get to where it is...
NSOpenGLLayer
Hope this helps....

Resources