I want to implement undo action after replacing portion of text in an NSTextView. I am replacing portion of text with following code
- (void)omeAction
{
NSString *fullString = [self.textView string];
NSRange selectedRange = [self.textView selectedRange];
NSString *selectedString = [fullString substringWithRange:selectedRange];
NSString *stringToReplace = ...;
[[self.textView textStorage] beginEditing];
[[self.textView textStorage] replaceCharactersInRange:selectedRange withString:stringToReplace];
[[self.textView textStorage] endEditing];
}
While performing undo I couldn't really undo the text replacement
From Cocoa Text Architecture Guide: Text Editing – Text Change Notifications and Delegate Messages:
In actually making changes to the text, you must ensure that the changes are properly performed and recorded by different parts of the text system. You do this by bracketing each batch of potential changes with shouldChangeTextInRange:replacementString: and didChangeText messages. These methods ensure that the appropriate delegate messages are sent and notifications posted. …
In my experience, that includes generating the relevant undo operation.
So, you would do:
if ([self.textView shouldChangeTextInRange:selectedRange replacementString:stringToReplace])
{
[[self.textView textStorage] beginEditing];
[[self.textView textStorage] replaceCharactersInRange:selectedRange withString:stringToReplace];
[[self.textView textStorage] endEditing];
[self.textView didChangeText];
}
First I tried to solve undo and shouldChangeTextInRange:replacementString: did the trick. However I found that insertText:replacementRange: had the same effect.
[self insertText:attributedString replacementRange:range];
Or:
if ([textView shouldChangeTextInRange:range replacementString:string]) { //string
[textStorage beginEditing];
[textStorage replaceCharactersInRange:range withAttributedString:attributedString];
[textStorage endEditing];
[textView didChangeText];
}
Related
I'm trying to use an NSLinguisticTagger to monitor the contents of an NSTextStorage and provide some contextual information based on what the user types. To that end, I have an OverlayManager object, which wires up this relationship:
-(void) setView:(NSTextView*) view {
_view = view;
_layout = view.layoutManager;
_storage = view.layoutManager.textStorage; //get the TextStorage from the view
[_tagger setString:_storage.string]; //pull the string out, this grabs the mutable version
[self registerForNotificationsOn:self->_storage]; //subscribe to the willProcessEditing notification
}
When an edit occurs, I make sure to trap it and notify the tagger (and yes, I know I'm being annoyingly inconsistent with member access, I'm rusty on Obj-C, I'll fix it later):
- (void) textStorageWillProcessEditing:(NSNotification*) notification{
if ([self->_storage editedMask] & NSTextStorageEditedCharacters) {
NSRange editedRange = [self->_storage editedRange];
NSUInteger delta = [self->_storage changeInLength];
[_tagger stringEditedInRange:editedRange changeInLength:delta]; //should notify the tagger of the changes- if I replace this line with [_tagger setString:[_storage string]] it works, but gobbles CPU as it retags the entire string
[self highlightEdits:self];
}
}
The highlightEdits message delegates the job out to a pool of "Overlay" objects. Each contains a block of code similar to this:
[tagger enumerateTagsInRange:range scheme:NSLinguisticTagSchemeLexicalClass options:0 usingBlock:^(NSString *tag, NSRange tokenRange, NSRange sentenceRange, BOOL *stop) {
if (tag == PartOfSpeech) {
[self applyHighlightToRange:tokenRange onStorage:storage];
}
}];
And that's where the problem is- the enumerateTagsInRange method crashes out with a message:
2014-06-04 10:07:19.692 WritersEditor[40191:303] NSMutableRLEArray replaceObjectsInRange:withObject:length:: Out of bounds
This problem doesn't occur if I don't link to the mutable copy of the underlying string and instead do a [[_storage string] copy], but obviously I don't want to copy the entire backing store every time I want to do tagging. This all should be happening in the main run loop, so I don't think this is a threading issue. The NSRange I'm enumerating tags on exists both in the NSTextStorage and in the NSLinguisticTagger's view of the string. It's not even the fact that the applyHighlightToRange call adds attributes to the string, because it crashes before even reaching that line.
I attempted to build a test case around the problem, but can't replicate it in those situations:
- (void) testEdit
{
NSAttributedString* str = [[NSMutableAttributedString alloc] initWithString:#"Quickly, this is a test."];
text = [[NSTextStorage alloc] initWithAttributedString:str];
NSArray* schemes = [NSLinguisticTagger availableTagSchemesForLanguage:#"en"];
tagger = [[NSLinguisticTagger alloc] initWithTagSchemes:schemes options:0];
[tagger setString:[text string]];
[text beginEditing];
[[text mutableString] appendString:#"T"];
NSRange edited = [text editedRange];
NSUInteger length = [text changeInLength];
[text endEditing];
[tagger stringEditedInRange:edited changeInLength:length];
[tagger enumerateTagsInRange:edited scheme:NSLinguisticTagSchemeLexicalClass options:0 usingBlock:^(NSString *tag, NSRange tokenRange, NSRange sentenceRange, BOOL *stop) {
//doesn't matter, this should crash
}];
}
That code doesn't crash.
I have a TextKit based editor with support to adding images. I want to show each image on separate lines.
My code looks like this (Thank you TextEdit), in my subclass of NSTextStorage
- (void)addImageAssets:(NSArray *)allAssets atRange:(NSRange)range;
{
NSMutableAttributedString *attachments = [NSMutableAttributedString new];
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.alignment = NSTextAlignmentCenter;
paragraphStyle.lineSpacing = 20.0f;
for (ALAsset *newAsset in allAssets)
{
UIImage *theImage = [UIImage imageWithCGImage:newAsset.aspectRatioThumbnail];
NSTextAttachment *textAttachment = [NSTextAttachment new];
textAttachment.image = theImage;
NSMutableAttributedString *replacementString = [NSMutableAttributedString new];
[replacementString appendAttributedString:[NSAttributedString attributedStringWithAttachment:textAttachment]];
[replacementString addAttribute:NSParagraphStyleAttributeName
value:paragraphStyle
range:NSMakeRange(0, [replacementString length])];
[attachments appendAttributedString:replacementString];
}
_isEditing = YES;
[self beginEditing];
[_backingStore replaceCharactersInRange:range
withAttributedString:attachments];
[self edited:NSTextStorageEditedAttributes
range:range changeInLength:allAssets.count];
[super processEditing];
[self endEditing];
_isEditing = NO;
}
(the _isEditing is a boolean flag used for book-keeping)
The output looks like this Output http://take.ms/QCvRK
I have tried various parameters on the NSMutableParagraphStyle but I couldn't get a line break after each image.
Appending a line break ("\r") around the text attachment will result in a glyph error
!!! _NSLayoutTreeLineFragmentRectForGlyphAtIndex invalid glyph index 1
!!! _NSGlyphTreeInvalidateGlyphsForCharacterRange invalid char range 1
!!! _NSGlyphTreeInvalidateGlyphsForCharacterRange character count mismatch
!!! _NSLayoutTreeLineFragmentRectForGlyphAtIndex invalid glyph index 0
!!! _NSLayoutTreeLineFragmentRectForGlyphAtIndex invalid glyph index 0
!!! _NSLayoutTreeLineFragmentUsedRectForGlyphAtIndex invalid glyph index 2147483647
I tried to subclass the NSTextAttachment to over-ride the attachmentBoundsForTextContainer:proposedLineFragment:glyphPosition:characterIndex: so that the width is set to the device width, this resulted in the attached image looking blown up.
Any suggestion on how I can introduce a line break right after each image ? Also it would be great if I can get the text to flow around the image attachments.
Thanks in advance.
Right now I'm using
NSShadow *textShadow = [NSShadow new];
textShadow.shadowBlurRadius = 5;
textShadow.shadowColor = [[NSColor whiteColor] colorWithAlphaComponent:.5];
[self addAttribute:NSShadowAttributeName value:textShadow range:NSMakeRange(0, self.length)];
from an NSTextStorage to give text a shadow. But I want to apply more than one shadow, and adding another NSShadowAttributeName just overwrites the previous value.
How can I add more than one shadow? Can it be done with CGContextSetShadowWithColor?
Not sure please try this below code for your textview. When you write string inside textview it will select that much range and on the basis of that it draws the color:-
-(IBAction)createNewTabView:(id)sender
{
NSString *allTheText =[tv string];
NSArray *lines = [allTheText componentsSeparatedByString:#"\n"];
NSString *str=[[NSString alloc]init];
NSMutableAttributedString *attr;
BOOL isNext=YES;
[tv setString:#""];
for (str in lines)
{
attr=[[NSMutableAttributedString alloc]initWithString:str];
if ([str length] > 0)
{
NSRange range=NSMakeRange(0, [str length]);
[attr addAttribute:NSBackgroundColorAttributeName value:[NSColor greenColor] range:range];
[tv .textStorage appendAttributedString:attr];
isNext=YES;
}
else
{
NSString *str=#"\n";
NSAttributedString *attr=[[NSAttributedString alloc]initWithString:str];
[tv .textStorage appendAttributedString:attr];
isNext=NO;
}
if (isNext==YES)
{
NSString *str=#"\n";
NSAttributedString *attr=[[NSAttributedString alloc]initWithString:str];
[tv .textStorage appendAttributedString:attr];
}
}
}
I would suggest the "caveman" approach. Instead of trying to get duplicate shadows, use duplicate Text's, all but one of which has it's color set to the clear color, and give them different dropShadows.
make two ore more text's that line up exactly.
make the front most your "real" text color.
make the other text's clear color.
set one type of shadow for each type of text.
You should be able to make a class that automates this, if you are going to use it multiple times.
This is a sample of what I got:
oh-- if you don't make the additional text's your text will appear much darker/bolder than you expect.
I am trying to put a Attributed string inside a NSTextField, which itself is inside an NSAlert
Here is my code:
NSTextField *label1 = [[NSTextField alloc]initWithFrame:NSMakeRect(0, 23, 50, 20)];
[label1 setEditable:FALSE];
[label1 setAllowsEditingTextAttributes:TRUE];
[label1 setBezeled:FALSE];
label1.backgroundColor = [NSColor clearColor];
NSString *login = #"Username";
NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:login];
NSString *boldFontName = [[NSFont boldSystemFontOfSize:12] fontName];
[attrString beginEditing];
NSRange ran = NSMakeRange(0, 8);
[attrString addAttribute:NSFontAttributeName
value:boldFontName
range:ran];
[attrString endEditing];
NSLog(#"%#",attrString);
[label1 setAttributedStringValue:attrString];
[alert setAccessoryView:label1];
[alert runModal];
However, as soon as [alert runModal] is called, my app crashes
"[__NSCFConstantString pointSize]: unrecognized selector sent to instance 0x7fff74035bb0"
I'm not sure why this is happening. It appears that it is related to the string, because as soon as i remove [alert setAccessoryView:label1] or give label1 a standard nsstring it works fine. Please help!
You have done right. But you did a small mistake. You have passed NSString as an attribute for NSFontAttributeName but it expects NSFont.
Try this.
NSFont *boldFontName = [NSFont boldSystemFontOfSize:12];
[attrString beginEditing];
NSRange ran = NSMakeRange(0, 8);
[attrString addAttribute:NSFontAttributeName
value:boldFontName
range:ran];
The above answer is absolutely correct. I had a same crash which used to crash in iOS 7.0.3 & 7.0.4 only, and works perfectly in all other versions. After so much investigation, I came to know that #"HelveticaNeue-Italic" is not available in iOS 7.0.3 & 7.0.4 versions, so that I used to get above crash in those versions.
I have fixed the issue with below code :
self.headerFont = [UIFont fontWithName:#"HelveticaNeue-Italic" size:16.0f];
if (self.headerFont == nil) {
self.headerFont = [UIFont fontWithName:#"HelveticaNeue" size:16.0f];
}
I need to set a text size (for example to 42) of the selected rich text which uses multiple fonts.
I imagine I can check attributes of each group of characters, modify the font size and set attributes back, but looking at the floating Font panel it seems like there should be a very easy and straightforward way to accomplish that. Do I miss something obvious?
On 10.6 there is a convenient way to iterate over the attributes and increase the font size.
This method can be added to an NSTextView category.
- (IBAction)increaseFontSize:(id)sender
{
NSTextStorage *textStorage = [self textStorage];
[textStorage beginEditing];
[textStorage enumerateAttributesInRange: NSMakeRange(0, [textStorage length])
options: 0
usingBlock: ^(NSDictionary *attributesDictionary,
NSRange range,
BOOL *stop)
{
#pragma unused(stop)
NSFont *font = [attributesDictionary objectForKey:NSFontAttributeName];
if (font) {
[textStorage removeAttribute:NSFontAttributeName range:range];
font = [[NSFontManager sharedFontManager] convertFont:font toSize:[font pointSize] + 1];
[textStorage addAttribute:NSFontAttributeName value:font range:range];
}
}];
[textStorage endEditing];
[self didChangeText];
}
Generalizing on Jonathan's answer a bit, here is a category interface you can simply paste into appropriate files in your Xcode project:
#interface NSTextView (FrameworkAdditions)
- (IBAction)decrementFontSize:(id)sender;
- (IBAction)incrementFontSize:(id)sender;
#end
And the corresponding implementation:
#implementation NSTextView (FrameworkAdditions)
- (void)changeFontSize:(CGFloat)delta;
{
NSFontManager * fontManager = [NSFontManager sharedFontManager];
NSTextStorage * textStorage = [self textStorage];
[textStorage beginEditing];
[textStorage enumerateAttribute:NSFontAttributeName
inRange:NSMakeRange(0, [textStorage length])
options:0
usingBlock:^(id value,
NSRange range,
BOOL * stop)
{
NSFont * font = value;
font = [fontManager convertFont:font
toSize:[font pointSize] + delta];
if (font != nil) {
[textStorage removeAttribute:NSFontAttributeName
range:range];
[textStorage addAttribute:NSFontAttributeName
value:font
range:range];
}
}];
[textStorage endEditing];
[self didChangeText];
}
- (IBAction)decrementFontSize:(id)sender;
{
[self changeFontSize:-1.0];
}
- (IBAction)incrementFontSize:(id)sender;
{
[self changeFontSize:1.0];
}
#end
This will double the font size, but you may change the scale property to any value, or provide your fixed size
NSFont * font = ...;
CGFloat fontSize = [[font fontDescriptor].fontAttributes[NSFontSizeAttribute] floatValue];
font = [NSFont fontWithDescriptor:[font fontDescriptor] size:fontSize * 2.];
self.textField.font = font;
Note: I assume that you are using a NSTextView and that you can access its text storage (NSTextStorage).
I think it is not possible to only change the font's size over a text that use multiple fonts. In NSAttributedString, font's size is part of the NSFontAttributeName attribute which controls both the font and the size.
One solution is to iterate over the selection and use the attribute:atIndex:longestEffectiveRange:inRange: to capture the range when each font apply, change the font's size and then use the addAttribute:value:range: to set the new font over the range.
Update:
If you take a look at the GNUstep GUI source code for NSTextView (under LGPL), you will see that their implementation use the range iteration.
Since NSTextView is a subclass of NSView, you can use -scaleUnitSquareToSize: to change the magnification level of the text view. For example, to make all the text double sized you'd call:
[textView scaleUnitSquareToSize:NSMakeSize(2.0, 2.0)];
You may need to make some adjustments to the dimensions of the text view's NSTextContainer after performing this operation to ensure the text is laid out correctly.