Strange changes to NSLayoutManager in 10.11 and higher - macos

My goal is to make text selection behaviour similar to Apple Pages, MS Word and any other text processor, when NSTextView selects ONLY text glyphs and not any indents or the whole NSTextContainer size like by default NSTextView's behavior.
With that goal in mind, I subclassed NSLayoutManager and overrided func rectArray(forCharacterRange charRange: NSRange, withinSelectedCharacterRange selCharRange: NSRange, in container: NSTextContainer, rectCount: UnsafeMutablePointer<Int>) -> NSRectArray?
In my override I simply calculated new rects that bound just glyphs and rejects any indents, blank spaces etc.
Everything worked fine. But suddenly in macOS 10.11 Apple did deprecate the method I'm using. And suggested to use func enumerateEnclosingRectsForGlyphRange: withinSelectedGlyphRange: inTextContainer: usingBlock: instead.
But it seems that this new method doesn't work in a way the previous deprecated method did. It's impossible to customize enclosing rects any more. All I can do is to enumerate through rects TextKit did calculate. And now it's impossible to achieve desirable text selection behaviour because the rects are read-only. And, by the way, the new method doesn't get called at all by the TextKit. So there's no point for me to think about it.
What should I do?

While the surprise with nonequivalent method replacement remains, I was able to find a solution.
It turned out that I don't need rectArrayForCharacterRange method to do the job. I just overrided func fillBackgroundRectArray(_ rectArray: UnsafePointer<NSRect>, count rectCount: Int, forCharacterRange charRange: NSRange, color: NSColor) and put my rect calculations in there.
Everything works like a charm now!

Related

How to implement a horizontal rule in a text view?

Say I have an NSTextView (or a UITextView on iOS) and I want the user to be able to insert horizontal divider rules, like the HTML <hr> tag or this thing:
What's the best way to implement this?
Apple's documentation is very thing on this. So far, I have two ideas:
Insert an NSTextAttachment for each rule and make the layout manager draw it somehow.
Instead of a single text view, use multiple text views with scrolling disabled, put them in a stack view, add separator views between them and then put the stack view in a scroll view.
Both approaches seem a little wrong or inelegant to me because:
From the documentation, it sounds like text attachments are intended for attaching files in the first place. A horizontal rule is not a file.
If I use multiple text views, I'll probably lose some performance tweaks inherent to NSTextView as all the text views need to be loaded and ready for display, no matter where the user has scrolled. In addition, the multiple text view approach would prevent the user from selecting the entire text, which is a requirement in my app.
Any better ideas?
I remember trying to do this years ago. Using NSTextAttachment was the method that worked for me. I bookmarked this conversation with Mike Ferris to help me remember how to do it. I don't have the code anymore, but it was pretty straightforward. I subclassed NSTextAttachmentCell which conforms to NSTextAttachmentCellProtocol. You override cellFrame(for textContainer: NSTextContainer, proposedLineFragment lineFrag: NSRect, glyphPosition position: NSPoint, characterIndex charIndex: Int) which gives you access to its container that can provide its width, and it has the line fragment which gives you the y position of the text above your divider (plus any padding you want). Then just override the draw(withFrame cellFrame: NSRect, in controlView: NSView?) method and draw your divider.
As for the payload NSData, you could do anything with that. Maybe you include the width of the line, it's padding, color, etc? The good thing about using NSTextAttachment is that it stays embedded in the text itself like the <hr> tag.

NSTextView insertion point not flashing

I’m attempting to update a Cocoa app for the first time in perhaps 8 years. It seems to build OK, and mostly runs fine too. I can edit text, but the insertion point doesn’t blink.
I use an NSTextView subclass to display text. I’m rusty at Cocoa, so I am guessing something changed with app napping or the like. Is there anything I need to do to make sure insertion points blink? More likely, what did I break to prevent periodic updating in the modern battery-friendly way?
I have the same problem with NSTextView and after multiples tests, it seems that bug happens with every basic textView on macOS 10.14 for me.
I've searched for multiple fixes on the web with no result. The best workaround I've found is to set the textView delegate to itself. Then, on didProcessEditing, call updateInsertionPointStateAndRestartTimer on the main thread as the following :
class CustomTextView: NSTextView {
override func awakeFromNib() {
super.awakeFromNib()
// 1. Set the textStorage delegate
textStorage?.delegate = self
}
}
extension CustomTextView: NSTextStorageDelegate {
func textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorageEditActions, range editedRange: NSRange, changeInLength delta: Int) {
// 2. On the main thread, update the insertion point
DispatchQueue.main.async {
self.updateInsertionPointStateAndRestartTimer(true)
}
}
OK, just in case this bites someone else: I had switched to dark mode. Apparently the default insertion point is white, so I couldn’t see it flash against the white background. So I need to properly support dark mode.

NSTextField resizable to fit content

I have NSTextField with left, right and top constraints defined (no bottom constraint set). I need NSTextField to grow if content can't fit in it and decrease size if there is unused space left.
Now I have: NSTextField automatically expands with strange behavior if it has multi-line text or too much content, also NSTextField doesn't decrease own's size on window resize.
I haven't found any simple solution written on Swift to solve that problem (I have a lot of such labels with constraints), at iOS everything was working with usual text labels and constraints.
I've created simple project for that question that you can see the problem: [Download Text.zip]
The solutions I've found but not used:
You can try to calculate possible TextField height and set height constraint for it. Problems of that solution:
Possible height calculations are inaccurate, sometimes you calculate incorrect height.
Solutions are written on Objective-C with some complex code.
Run .sizeToFit() on each window resize or text change action. It's not working because .sizeToFit() always compress all text to single line.
Use NSTextView instead of NSTextField. It's good way, but:
I don't need scrolling, editing and other functional of NSTextView. I don't want to call to complex component for simple label.
NSTextView always wants height or bottom constraint, I don't know bottom constraint because content can expand down with new text.
I haven't find full solution to make NSTextView's behavior like I want :)
According to Mac OS X Release Notes (section NSTextField intrinsicContentSize improvements) it’s known bug when height of NSTextField is changed, but width is remained the same. We have two ways to fixing it:
We can specify maximumNumberOfLines to a value that makes sense. That is not good way to us because we don’t know actual number of lines and don’t want to calculate it.
We can set preferredMaxLayoutWidth to a real value. I’ve ended with such code:
Code:
override func viewDidLayout() {
super.viewDidLayout()
textField.preferredMaxLayoutWidth = textField.frame.width
}

How to get the source lists selection highlight to use the Dark Vibrancy appearance in OS X 10.10?

In OS X 10.10 source lists seem to use the light vibrancy appearance. In the Finder (and in some other third party applications, Things.app for example) the selected item in the source list is indicated by a dark vibrancy appearance. For example, see the Desktop row in the image below.
How can I replicate this behaviour? Do I need to use the delegate methods to specify the table row view,
-outlineView:rowViewForItem:
and attempt custom drawing myself or is there a more straight forward approach? If you make a standard source list UI in Xcode the default highlighting is remain the standard blue rectangle that we have seen in previous version of OS X.
After playing around for a while I found a way to accomplish this.
It turned out that I would get the "Finder highlight" style when using NSTableViewSelectionHighlightStyleSourceList and clicking outside my NSOutlineView. So I figured it would stay that way if you refuse making it first responder.
Simply make your NSOutlineView a subclass and override this method:
-(BOOL)acceptsFirstResponder{
return NO;
}
It works, but with some downsides. For example, using arrow keys in the NSOutlineView will no longer work. I downloaded the Things app, and it does not allow use of arrow keys either, so it's very likely that this is how they are doing it. If anyone finds a better way, please post it.
Here is the Swift equivalent :
func outlineView(_ outlineView: NSOutlineView, rowViewForItem item: Any) -> NSTableRowView? {
return CustomTableRowView(frame: NSZeroRect);
}
And the subclass of NSTableRowView
import Cocoa
class CustomTableRowView: NSTableRowView {
override var isEmphasized: Bool {
set {}
get {
return false;
}
}
}
If you would like to keep arrow keys working, you can subclass NSTableRowView and override the following method:
- (BOOL)isEmphasized
{
return NO;
}
I am not sure, this is "Dark Vibrancy".
I would rather try setting the background color to something like "Alternate Selected Control Text Color"
Have a look at an NSTextField in InterfaceBuilder. there are many "Control Text" colors, which have a special appearance on visual effect views.
and for setting the selection color see this answer (untested):
NSTableview Change the highlight colour

Links in NSTableView NSCell

I have been reading and experimenting with allowing links in a custom drawn NSCell for the last few days and have basically got nothing usable, there's always issues with each approach.
Does anyone know of a way of doing this that works?
I am custom drawing the NSCell using - (void)drawInteriorWithFrame:(NSRect)theCellFrame inView:(NSView *)theControlViewm
The NSCell is just a variable height block of text with links inside it, some cells have links, some do not.
I've tried using nsattributedstring with NSLinkAttributeName
I've tried intercepting all hits to the cell and then trying to match up where they clicked to where the link would be in the text but that never works out.
I've basically tried all suggestions that I could find on all boards but most comments are old so I'm hoping someone has figured out a good way to do this.
Thanks, David
I haven't tried this exactly, but give this a try:
First, for hyperlinks I use a category on NSAttributedString, akin to this post from Apple developer docs. The example here gives you a method on NSAttributedString 'hyperlinkFromString:withURL:`
Second, create a delegate for the table, and implement tableView:willDisplayCell:forTableColumn:row: method.
In that method,
setAttributedStringValue:[NSAttributedString hyperlinkFromString:YOUR_STRING withURL:YOUR_URL]
or, if you need non-hyperlinked string text as well,
setAttributedStringValue:[SOME_NON_HYPERLINKED_STRING appendAttributedString:[NSAttributedString hyperlinkFromString:YOUR_STRING withURL:YOUR_URL]]
If that is the only reason you are custom drawing an NSCell, you can try getting rid of your custom class, because this should work with an NSTextFieldCell. I've seen online, though, that some people have had trouble with centering attributed strings in text field cells, so I hope it works ok. One other caveat: with the delegate method, be sure that you set the cell attribute that you are changing for all conditions. I quote:
Because aCell is reused for every row in aTableColumn, the delegate must set the display attributes both when drawing special cells and when drawing normal cells.
from "http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/NSTableViewDelegate_Protocol/Reference/Reference.html" (sorry, StackOverflow won't let me post more than one hyperlink yet)
Hope this helps.

Resources