Appending *simple* ASCII strings to an NSScrollView - objective-c

Is there a simple way to append a string to an NSScrollView's NSTextView? I don't want attributes. I simply have error messages coming in as NSString's, and I wish to append each to the window. Nothing fancy. No formatting beyond CR, LF and perhaps TAB if I have too many beers and decide to get over the top fancy.
Every path I follow through the Class docs seems to lead down into a self referential blackhole... Like NSMutableAttributedStrings... which aren't really NSStrings, and don't even have a cString method.
I have been considering just keeping my own NSString and complete rewriting the contents of the scroll view after appending the errorstring the easy way. But that seems... inefficient... when the numbers of reports could get quite large.

Every string needs some attributes to be drawn to the screen — a font/size/colour at the very least is mandatory.
It's not entirely obvious from the docs, but the "proper" way to manipulate an NSTextView is by manipulating the NSTextStorage directly. Also, NSTextStorage is a subclass of NSMutableAttributedString.
You can add characters to the string without dealing with attributes, it will simply copy the attributes from the string around where you add the text to:
[textView.textStorage replaceCharactersInRange:NSMakeRange(textView.textStorage.length, 0) withString:#"\nhello world"];
You will probably also want to scroll down:
[textView scrollRangeToVisible:NSMakeRange(textView.storage.length, 0)];
Performance will be good, even up to gigabytes of data. NSTextView is very efficient, especially when only small changes to the content are made.

Related

NSTextField accessibility - How to provide alternate text for voiceover

It will probably be obvious, but I have never done any work with NSAccessibility before so I'm assuming what I'm asking is something simple I've overlooking.
I have an NSTextField displaying a duration like this, 15:39. This text field is a subview of an NSTableCellView in a view-based NSTableView.
When VoiceOver is enabled, it currently reads off, "one five, three nine", which seems completely useless. Instead, I want it to say "Duration is 15 minutes, 39 seconds."
I can produce the desired string, but I cannot figure out which accessibility attributes I have to set to make this happen.
I've tried setting the accessibility description in IB, which has no effect (whether setting it on the NSTextField or NSTextFieldCell).
I've also tried overriding accessibilityValue: and accessibilityAttributeValue:forParameter: in order to provide custom attribute values for:
NSAccessibilityNumberOfCharactersAttribute
NSAccessibilityStringForRangeParameterizedAttribute
NSAccessibilityAttributedStringForRangeParameterizedAttribute
NSAccessibilityStringForRangeParameterizedAttribute.
This seemed to be the right track since that does allow me to replace what is read aloud by voice over, however, providing any NSRange for NSAccessibilityVisibleCharacterRangeAttribute that doesn't match the length of the "15:39" string causes voice over to completely skip this field when reading off the NSTableCellView's contents. So, the best I've been able to do is get Voice over to say "Durat" instead of reading off "15:39" :(
Everything I've tried, I've tried on NSTextField and NSTextFieldCell.
Ideally, I'd prefer to do what I'd do in iOS and just set the accessibilityLabel of the NSTableCellView, but I see no reasonable way of doing this in AppKit. Hopefully I'm just missing something.
I was able to achieve this simply by setting accessibilityValueDescription on the NSTextField. This method is part of the new Accessibility API on OS X 10.10 and higher. With the older API, you may be able to use kAXValueDescriptionAttribute to achieve the same thing.
The solution for overriding the text read by Voice Over was much simpler than I thought. All I had to do was override the value returned for NSAccessibilityAttributedStringForRangeParameterizedAttribute:
// The displayed text for this text field is "45m".
// The voice over for this field incorrectly reads "forty five meters" instead of "forty five minutes".
// The following forces it to read "Duration forty five minutes"
-(id)accessibilityAttributeValue:(NSString *)attribute forParameter:(id)parameter {
return ([attribute isEqualToString:NSAccessibilityAttributedStringForRangeParameterizedAttribute])
? [[NSAttributedString alloc] initWithString:#"Duration: 45min"];
: [super accessibilityAttributeValue:attribute forParameter:parameter];
}
I believe I've answered my own question, at least enough to control what's read off when clicking a table cell view.
By overriding accessibilityIsIgnored to return "NO" in my NSTableCellView sub-class I was able to specify exactly what I wanted to be read off for the table cell by overriding the NSAccessibilityTitleAttribute for the cell. I had not tried this before b/c I had misunderstood the purpose of the accessibilityIsIgnored selector.
From the documentation for accessibilityIsIgnored:
When asking for an object’s children, ignored children should not be included; instead, the ignored children should be replaced by their own unignored children.
I'd like to be able to control exactly what is read off for the individual NSTextFields in the future, but controlling what is read off for an entire NSTableCellView is actually ideal for my particular situation.

How to insert a NSButton into a NSTextView? (inline)

I have a NSTextView and a custom NSButton. What I want to do is to insert that button (60x16 in size) to the end of the NSTextView.
How can I do something like that? I've been trying to search around on how to do this but I'm not getting anywhere.
Where should I begin? Thanks
I believe this question is pretty similar to yours:
Buttons inside an NSTextView / NSTextStorage
Quote from the question:
how do I get an NSButton to appear inside the text and react to
clicks?
Note that the issue is not fully solved there, but it seems the OP got a good head start. Hopefully you can take some clues from the discussion.
Here is one answer:
NSTextAttachment holds the contents of an attachment; it is the value
of the NSAttachmentAttributeName attribute for the
NSAttachmentCharacter in the attributed string. The contents are
usually given by an NSFileWrapper, but this is not required; you can
create an empty NSTextAttachment with a nil file wrapper.
NSTextAttachmentCell handles display and user interaction for the
attachment. By default an NSTextAttachment will create an
NSTextAttachmentCell to display itself, depending on the contents of
the attachment; in the generic case this will just be an image cell
displaying an icon.
If you wish, however, you can supply a custom NSTextAttachmentCell for
your attachment. It need not be an member of the class
NSTextAttachmentCell; it only needs to conform to the
NSTextAttachmentCell protocol. Actually, even that is not strictly
necessary; it only needs to implement a few of the basic methods for
sizing and drawing. Most cells already do this.
You will, however, need to deal with mouse events yourself. The
methods you'll probably want to implement would be
wantsToTrackMouseForEvent:inRect:ofView:atCharacterIndex: and
trackMouse:inRect:ofView:atCharacterIndex:. The character index here
should let you determine which portion of the text is relevant.

Autoresizing NSTextView and it's Font Size

I'm trying to make my NSWindow autoresizable. I've gotten most of my UI items to resize, but one of the few remaining objects that pose issues are NSTextViews: indeed, I can't find a way to calculate the new font size once the NSTextView has been resized.
For NSTextFields, I had found a method that would find the font size based on the length of the text. Apparently, there doesn't seem to be an equivalent method for multi-line text containers (unless I just haven't found it).
The only actual place I found that mentionned something of the sort is here: http://boutcher.tumblr.com/post/7842216960/nstextview-auto-resizing-text
However, I wasn't able to implement this code into my application, as there seems to be an error I can't fix with the way an NSLayoutManager is created.
Has anyone done this in the past? I'm considering just allowing the user to resize to just one size, so I can hardcode the font size... It's a real pain dealing with these NSTextViews !
See the sizeWithAttributes: method in “NSString Application Kit Additions Reference.”
It returns an NSSize, which you can compare to the textview’s current frame.size.
For the “Attributes” arg make an NSDictionary with an NSFont as the object and NSFontAttributeName as the key. (Don't be confused by that constant. It looks like it's a key for a string, but it is not; it is a key for the font itself.)
Get the string from the textview: [[yourTextView textStorage] string].
Get the familyName of the font you are using and its current point size, a CGFloat. Compose fonts to test using the constructor [NSFont fontWithName:familyName size:floatChanged].
Once you've arrived at the correctly sized font, use it to make a new NSAttributedString out of the old string. Just use the "attributes" dictionary you made above (the one that produced the correct size) and feed it to NSAttributedString's initWithString:attributes constructor.
Assign that attributed string to the textStorage (itself a subclass of NSMutableAttributedString): [[yourTextView textStorage] setAttributedString:thatYouJustMade].
Note: If the textview employs attributes like underlining or fore/background coloring, or italicized/bold fonts, you must get that information from the textStorage and incorporate it into the new attributed string. It's doable. Look at the class refs for NSAttributedString and NSMutableAttributedString.

Switching textstorage of NSTextViews back and forth

I'm trying to make a feature in a product which gives the user the ability to split a textview into two. The way this is done is by removing the textview from it's superview, making a NSSplitView and adding the textview as well as a new NSTextView instance to this splitview. Lastly I make these two textviews share the same textstorage in order to make them share the same content.
It works great. But the problem is when I want to make one of the two textviews change textstorage. The replaceTextStorage method in NSLayoutManager causes both NSTextView to change textStorage. The API documentation states:
replaceTextStorage: All
NSLayoutManager objects sharing the
original NSTextStorage object then
share the new one. This method makes
all the adjustments necessary to keep
these relationships intact, unlike
setTextStorage:.
So it makes sense that it would do this. But the question is how do I make it possible to have two (or more) textviews first share the same storage and after that having them using their own?
I've tried replacing the layoutManager and even making new instances of NSTextViews but no luck...
Any suggestions?
If you want the two NSTextView to have difference NSTextStorage, then you have to create two parallel text systems.
First, start with the following architecture (this the one describe in the Text System Overview):
A common NSTextStorage
A first branch with NSLayoutManager/NSTextContainer/NSTextView
A second branch with NSLayoutManager/NSTextContainer/NSTextView
Then, call the replaceTextStorage: method on each NSLayoutManager to set different NSTextStorage.

Maintain Undo In Modified, Bound, NSAttributedString

I've got an attributed string bound to a NSTextView. I'm using a method that is called (using KVO) every time the string is edited to add background color attributes to string based on a regEx match. I do this by creating a new mutable attributed string with -initWithAttributedString: then -beginEditing, -addAttribute:, -endEditing. Once I've added all the background color attributes I want, I call the string's setter [self setTextViewString:mutableAttributedString] The issue is, that if there actually are any attributes added to the string, it kills undo and moves the cursor to the end of the string.
How can I maintain undo? I've maintained cursor position by calling the textView's selectedRanges and setSelectedRanges: methods on either side of the setter, but this still seems a bit hackish.
I wasn't able to bind the textview directly to the mutableattributedstring, but it seems like there should be a more direct way to modify the bound string so it doesn't mess up editing.
PS, the addition of attributes happens after the KVO method finishes by calling -performSelectorOnMainThread: It was the only way I could get the added attributes to display.
[self setTextView:mutableAttributedString]
Pardon? You're setting your textView to an attributed string? Wouldn't it make more sense to keep your text view there?
Try getting the text view's textStorage and replacing its contents with the new attributed string by sending the text storage a setAttributedString: message.