NSTextAttachmentCell is a mile high - objective-c

I'm editing a subset of HTML in an NSTextView[1] and I want to simulate an <hr> tag.
I've figured out that the way to do it is with NSTextAttachment and a custom NSTextAttachmentCell, and have the code all written to insert the attachment and cell. The problem is, there's an enormous amount of blank space below the cell.
This space is not part of the cell itself—if I paint the entire area of the cell red, it's exactly the right size, but the text view is putting the next line of text very far below the red. The amount seems to depend on how much text is above the cell; unfortunately, I'm working with long documents where <hr> tags are crucial, and this causes major problems with the app.
What the heck is going on?
The money parts of my cell subclass:
- (NSRect)cellFrameForTextContainer:(NSTextContainer *)textContainer
proposedLineFragment:(NSRect)lineFrag glyphPosition:(NSPoint)position
characterIndex:(NSUInteger)charIndex {
lineFrag.size.width = textContainer.containerSize.width;
lineFrag.size.height = topMargin + TsStyleBaseFontSize *
heightFontSizeMultiplier + bottomMargin;
return lineFrag;
}
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
characterIndex:(NSUInteger)charIndex
layoutManager:(NSLayoutManager *)layoutManager {
NSRect frame = cellFrame;
frame.size.height -= bottomMargin;
frame.size.height -= topMargin;
frame.origin.y += topMargin;
frame.size.width *= widthPercentage;
frame.origin.x += (cellFrame.size.width - frame.size.width)/2;
[color set];
NSRectFill(frame);
}
[1] I tried a WebView with isEditable set and the markup it produced was unusably dirty—in particular, I couldn't find a way to wrap text nicely in <p> tags.
To answer Rob Keniger's request for the code that inserts the horizontal rule attachment:
- (void)insertHorizontalRule:(id)sender {
NSAttributedString * rule = [TsPage newHorizontalRuleAttributedStringWithStylebook:self.book.stylebook];
NSUInteger loc = self.textView.rangeForUserTextChange.location;
if(loc == NSNotFound) {
NSBeep();
return;
}
if(loc > 0 && [self.textView.textStorage.string characterAtIndex:loc - 1] != '\n') {
NSMutableAttributedString * workspace = rule.mutableCopy;
[workspace.mutableString insertString:#"\n" atIndex:0];
rule = workspace;
}
if([self.textView shouldChangeTextInRange:self.textView.rangeForUserTextChange replacementString:rule.string]) {
[self.textView.textStorage beginEditing];
[self.textView.textStorage replaceCharactersInRange:self.textView.rangeForUserTextChange withAttributedString:rule];
[self.textView.textStorage endEditing];
[self.textView didChangeText];
}
[self.textView scrollRangeToVisible:self.textView.rangeForUserTextChange];
[self reloadPreview:sender];
}
And the method in TsPage that constructs the attachment string:
+ (NSAttributedString *)newHorizontalRuleAttributedStringWithStylebook:(TsStylebook*)stylebook {
TsHorizontalRuleCell * cell = [[TsHorizontalRuleCell alloc] initTextCell:#"—"];
cell.widthPercentage = 0.33;
cell.heightFontSizeMultiplier = 0.25;
cell.topMargin = 12.0;
cell.bottomMargin = 12.0;
cell.color = [NSColor blackColor];
NSTextAttachment * attachment = [[NSTextAttachment alloc] initWithFileWrapper:nil];
attachment.attachmentCell = cell;
cell.attachment = attachment;
NSAttributedString * attachmentString = [NSAttributedString attributedStringWithAttachment:attachment];
NSMutableAttributedString * str = [[NSMutableAttributedString alloc] initWithString:#""];
[str appendAttributedString:attachmentString];
[str.mutableString appendString:#"\n"];
return str;
}

Try changing your cell class's cellFrameForTextContainer:proposedLineFragment:glyphPosition: method to return NSZeroPoint for the origin of the cell's frame.
(I have no idea why this should work, but the questioner and I have been live-debugging it, and it actually does.)
Added by questioner: It looks like, even though the rect being returned is described as a "frame", its origin is relative to the line it's in, not to the top of the document. Thus, the return value's origin.y value should be set to zero if you want it on the same line.
(The origin.x value, on the other hand, does refer to the cell's position in the line, so it should be the same as lineFrag.origin.x unless you want to change the cell's horizontal location.)

Related

Objective C - Append ellipsis to a UILabel that is centrally aligned, but don't move text

I have a UILabel that is near 100% width. The text is centrally aligned.
I have some code to append or otherwise walkthrough the addition of ellipses.
However this causes an issue where the UILabel moves whilst the animation occurs. This is because the text is centrally aligned.
I would like my code to add or otherwise update the ellipses but not "move" the UILabel.
I don't want to be adding another label if I can help it, but if thats the only solution then cool.
The code follows;
// This is a method inside a custom UILabel subclass
- (void)startProgressAnimationWithInterval:(NSTimeInterval)interval
{
self.isAnimatingProgress = YES;
NSString *singleDot = #".";
NSString *doubleDot = #"..";
NSString *tripleDot = #"...";
if (![self.text hasSuffix:tripleDot]) {
self.text = [self.text stringByAppendingString:tripleDot];
}
self.progressTimer = [NSTimer vic_scheduledTimerWithTimeInterval:interval userInfo:nil action:^(NSTimer *timer, NSInteger repeatIndex) {
if ([self.text hasSuffix:tripleDot]) {
self.text = [self.text stringByReplacingOccurrencesOfString:tripleDot withString:singleDot];
}
else if ([self.text hasSuffix:doubleDot]) {
self.text = [self.text stringByReplacingOccurrencesOfString:doubleDot withString:tripleDot];
}
else {
self.text = [self.text stringByReplacingOccurrencesOfString:singleDot withString:doubleDot];
}
}];
}
There are a bunch of ways of tackling this. For example, rather than changing the text, set the text to include the ellipses, but then just set the font color of a variable number of dots of the ellipses to a clear color of the attributedText of the label:
- (void)handleTimer:(NSTimer *)timer {
if (++self.dotsToShow >= 4) self.dotsToShow = 0; // an integer state that rotates 0, 1, 2, 3 and then repeats
NSInteger dotsToHide = 3 - self.dotsToShow;
NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:self.label.text];
if (dotsToHide > 0) {
[string setAttributes:#{NSForegroundColorAttributeName: [UIColor clearColor]} range:NSMakeRange(string.length - dotsToHide, dotsToHide)];
}
self.label.attributedText = string;
}

NSString font size specific to frame width

I am using drawRect for a text display, calling NSString. I am trying to implement using sizeWithFont to auto resizing font (shrinking) with default font size of 17 and using a loop to reduce the font size by 1 if it does not fit the size of width. Can anyone help me how to implement this? Example would be nice right now I just have the font size set to 17.0
[[self.string displayName] drawAtPoint:CGPointMake(xcoord, ycoord) withFont:[UIFont boldSystemFontOfSize:17.0]];
CGSize size = [[self.patient displayName] sizeWithFont:[UIFont boldSystemFontOfSize:17.0]];
max_current_y = size.height > max_current_y ? size.height : max_current_y;
xcoord = xcoord + 3.0f + size.width;
OK never mind. Here's modified version of the same method that takes NSString for which to return a font:
-(UIFont*)getFontForString:(NSString*)string
toFitInRect:(CGRect)rect
seedFont:(UIFont*)seedFont{
UIFont* returnFont = seedFont;
CGSize stringSize = [string sizeWithAttributes:#{NSFontAttributeName : seedFont}];
while(stringSize.width > rect.size.width){
returnFont = [UIFont systemFontOfSize:returnFont.pointSize -1];
stringSize = [string sizeWithAttributes:#{NSFontAttributeName : returnFont}];
}
return returnFont;
}
Here's how to call it:
NSString* stringToDraw = #"Test 123";
CGRect rect = CGRectMake(100., 100., 100., 200.);
UIFont* font = [self getFontForString:stringToDraw toFitInRect:rect seedFont:[UIFont systemFontOfSize:20]];
[stringToDraw drawInRect:rect withFont:font];
Code is for iOS7+
Trying font sizes with step 1.0 may be very slow. You can tremendously improve the algorithm by making two measures for two different sizes, then using linear approximation to guess the size that will be very close to the right one.
If it turns out not close enough, repeat the calculation using the guessed size instead of one of the previous two until it is good enough or stops changing:
// any values will do, prefer those near expected min and max
CGFloat size1 = 12.0, size2 = 56.0;
CGFloat width1 = measure_for_size(size1);
CGFloat width2 = measure_for_size(size2);
while (1) {
CGFloat guessed_size = size1 + (required_width - width1) * (size2 - size1) / (width2 - width1);
width2 = measure_for_size(guessed_size);
if ( fabs(guessed_size-size2) < some_epsilon || !is_close_enough(width2, required_width) ) {
size2 = guessed_size;
continue;
}
// round down to integer and clamp guessed_size as appropriate for your design
return floor(clamp(guessed_size, 6.0, 24.0));
}
is_close_enough() implementation is completely up to you. Given that text width grows almost linearly of font size, you can simply drop it and just do 2-4 iterations which should be enough.
I wanted to try to make a version that didn't have to repeatedly check font sizes using a do...while loop. Instead, I assumed that font point sizes were a linear scale, then worked out the size difference between the required frame width and the actual frame width, then adjusted the font size accordingly. Therefore, I ended up with this function:
+ (CGFloat)fontSizeToFitString:(NSString *)string inWidth:(float)width withFont:(UIFont *)font
{
UILabel *label = [UILabel new];
label.font = font;
label.text = string;
[label sizeToFit];
float ratio = width / label.frame.size.width;
return font.pointSize * ratio;
}
Pass in a font of any size, as well as the string and the required width, and it will return you the point size for that font.
I also wanted to take it a bit further and find out the font size for a multi-line string, so that the longest line would fit without a line break:
+ (CGFloat)fontSizeToFitLongestLineOfString:(NSString *)string inWidth:(float)width withFont:(UIFont *)font
{
NSArray *stringLines = [string componentsSeparatedByString:#"\n"];
UILabel *label = [UILabel new];
label.font = font;
float maxWidth = 0;
for(NSString *line in stringLines)
{
label.text = line;
[label sizeToFit];
maxWidth = MAX(maxWidth, label.frame.size.width);
}
float ratio = width / maxWidth;
return font.pointSize * ratio;
}
Seems to work perfectly fine for me. Hope it helps someone else.
Original poster didn't specify what platform he was working on, but for OSX developers on Mavericks, sizeWithFont: doesn't exist and one should use sizeWithAttributes :
NSSize newSize = [aString sizeWithAttributes:
[NSDictionary dictionaryWithObjectsAndKeys:
[NSFont fontWithName:#"Arial Rounded MT Bold" size:53.0],NSFontAttributeName,nil
]];
Here's a method which can return you font that will fit in a rect:
-(UIFont*)getFontToFitInRect:(CGRect)rect seedFont:(UIFont*)seedFont{
UIFont* returnFont = seedFont;
CGSize stringSize = [self sizeWithFont:returnFont];
while(stringSize.width > rect.size.width){
returnFont = [UIFont systemFontOfSize:returnFont.pointSize -1];
stringSize = [self sizeWithFont:returnFont];
}
return returnFont;
}
You can add this method to a NSString category. You can find more about how to add a category here: http://developer.apple.com/library/ios/#documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html#//apple_ref/doc/uid/TP40011210-CH6-SW2
If you don't want to create a category, you can add this method to one of your utility classes and pass in the string for which you want the font to be returned.
Here is another method, inspired by #puru020 & #jowie answers. Hope it helps someone
-(UIFont *) adjustedFontSizeForString:(NSString *)string forWidth:(float)originalWidth forFont:(UIFont *)font
{
CGSize stringSize = [string sizeWithFont:font];
if(stringSize.width <= originalWidth)
{
return font;
}
float ratio = originalWidth / stringSize.width;
float fontSize = font.pointSize * ratio;
return [font fontWithSize:fontSize];
}
I modified a bit the solution of #puru020 , added the support for attributes, and improved a bit:
Note: The method should be wrapped in a NSString Category
- (UIFont*)requiredFontToFitInSize:(CGSize)size seedFont:(UIFont*)seedFont attributes:(NSDictionary*)attributes{
UIFont *returnFont = [UIFont systemFontOfSize:seedFont.pointSize +1];
NSMutableDictionary *mutableAttributes = attributes.mutableCopy;
CGSize stringSize;
do {
returnFont = [UIFont systemFontOfSize:returnFont.pointSize -1];
[mutableAttributes setObject:returnFont forKey:NSFontAttributeName];
stringSize = [self sizeWithAttributes:mutableAttributes];
} while (stringSize.width > size.width);
return returnFont;
}

How can I wrap the text added to the plot using core plot library in iphone sdk

I had added labels to the pie chart like this
-(CPTLayer *)dataLabelForPlot:(CPTPlot *)plot recordIndex:(NSUInteger)index
{
CPTTextLayer *newLayer = nil;
UIFont *theFont;
static CPTMutableTextStyle *whiteText = nil;
if ( !whiteText )
{
whiteText = [[CPTMutableTextStyle alloc] init];
whiteText.color = [CPTColor blackColor];
whiteText.textAlignment = CPTTextAlignmentCenter;
whiteText.fontName = #"Helvetica-Bold";
whiteText.fontSize = 10.0f;
theFont = [UIFont fontWithName:whiteText.fontName size:whiteText.fontSize];
}
if ( [plot isKindOfClass:[CPTPieChart class]] )
{
newLayer.delegate = self;
newLayer = [[[CPTTextLayer alloc] initWithText:[NSString stringWithFormat:#"%#", [pieChartData2 objectAtIndex:index]] style:whiteText] autorelease];
}
return newLayer;
}
What I want here is
(1)I want to wrap the text which is too long.
(2) I want to display the the Slice title and and its value .
(3) I need to draw a line to title and Slice .
Please any one help me.
thanks in advance
You can insert newline characters ('\n') in the text to wrap to the next line. Core Plot does not auto-wrap any text.
This is a datasource method—it should have access to the data values and the title. Use both to build the label string.
Drawing lines to a label is not supported yet. You could make a custom subclass of CPTTextLayer that draws the line in addition to the text and use that instead of the stock CPTTextLayer to make the labels.

How to create a PDF in iOS programmatically?

I want to create a PDF file in iOS ,there should be one table in the PDF, which is filled from one array. I already searched on Google, but didn't succeed. Any help is appreciated
Something like this routine to render the text:
- (CFRange)renderTextRange:(CFRange)currentRange andFrameSetter:(CTFramesetterRef)frameSetter intoRect:(CGRect)frameRect {
CGMutablePathRef framePath = CGPathCreateMutable();
CGPathAddRect(framePath, NULL, frameRect);
CTFrameRef frameRef = CTFramesetterCreateFrame(frameSetter, currentRange, framePath, NULL);
CGContextRef currentContext = UIGraphicsGetCurrentContext();
CGContextSaveGState(currentContext);
CGContextSetTextMatrix(currentContext, CGAffineTransformIdentity);
CGContextTranslateCTM(currentContext, 0, 792);
CGContextScaleCTM(currentContext, 1.0, -1.0);
CTFrameDraw(frameRef, currentContext);
CGContextRestoreGState(currentContext);
CGPathRelease(framePath);
currentRange = CTFrameGetVisibleStringRange(frameRef);
currentRange.location += currentRange.length;
currentRange.length = 0;
CFRelease(frameRef);
return currentRange;
}
And the following code snippet which calls it, assuming you have the context and any fonts etc created and in the appropriate variables. The following loop simply builds up the text line by line into an NSMutableAttributedString which you can then render:
CTFontRef splainFont = CTFontCreateWithName(CFSTR("Helvetica"), 10.0f, NULL);
CGFloat margin = 32.0f;
CGFloat sink = 8.0f;
NSMutableAttributedString *mainAttributedString = [[NSMutableAttributedString alloc] init];
NSMutableString *mainString = [[NSMutableString alloc] init];
// Ingredients is an NSArray of NSDictionaries
// But yours could be anything, or just an array of text.
for (Ingredient *ingredient in ingredients) {
NSString *ingredientText = [NSString stringWithFormat:#"%#\t%#
\n",ingredient.amount,ingredient.name];
[mainString appendString:ingredientText];
NSMutableAttributedString *ingredientAttributedText =
[[NSMutableAttributedString alloc] initWithString:ingredientText];
[ingredientAttributedText addAttribute:(NSString *)(kCTFontAttributeName)
value:(id)splainFont
range:NSMakeRange(0, [ingredientText length])];
[mainAttributedString appendAttributedString:ingredientAttributedText];
[ingredientAttributedText release];
}
Now you have your array written out with newlines to one NSMutableAttributedString you can render it, depending on your text you might want to render it out in a loop until the rendered location matches your text length. Something like:
// Render Main text.
CTFramesetterRef mainSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)mainAttributedString);
currentRange = [KookaDIS renderTextRange:currentRange
andFrameSetter:mainSetter
intoRect:pageRect];
// If not finished create new page and loop until we are.
while (!done) {
UIGraphicsBeginPDFPageWithInfo(pageRect, nil);
currentRange = [self renderTextRange:currentRange
andFrameSetter:mainSetter
intoRect:pageRect];
if (currentRange.location >= [mainString length]) {
done = TRUE;
}
}
The above code will need quite a bit of adapting I'm sure as it's hacked out of a project of my own so some variables (like the frame setter) won't exist and you need to close off the PDF context and release variables etc. Note how mainString is used to determine when the text has been rendered out.
It should give a clear enough indication of how to loop around an array or any other group to render an arbitrary length of text into a document.
Slight modifications to the while loop and the one render before entering it will let you render text out in multiple columns too.

Objective-C: Get closest NSView to dragged NSView

If I have a NSView with 10 subviews, and I drag one of the subviews, what is the easiest way to determine which of the remaining views is closes to the dragged one? My code is working but I somehow feel I'm using a monkey wrench to fine tune a violin. Is there a more elegant way?
subviews is an array of the subviews of the parent view of this view (so it includes this view)
the toolTip of the sub views contain their page # formatted like "Page_#"
- (void) mouseUp: (NSEvent* ) e {
//set up a ridiclous # for current distance
float mtDistance = 12000000.0;
//get this page number
selfPageNum = [[[self toolTip] substringFromIndex:5] intValue];
//set up pageViews array
pageViews = [[NSMutableArray alloc] init];
//loop through the subviews
for (int i=0; i<[subviews count]; i++) {
//set up the view
thisView = [subviews objectAtIndex:i];
//filter for view classes
NSString* thisViewClass = [NSString stringWithFormat:#"%#", [thisView className]];
if ( [thisViewClass isEqual:#"Page"]) {
//add to the pageViews array
[pageViews addObject:thisView];
if (self != thisView) {
//get the view and self frame
NSRect movedViewFrame = [self frame];
NSRect thisViewFrame = [thisView frame];
//get the location area (x*y)
float movedViewLoc = movedViewFrame.origin.x * (movedViewFrame.origin.y * -1);
float thisViewLoc = thisViewFrame.origin.x * (thisViewFrame.origin.y * -1);
//get the difference between x locations
float mtDifference = movedViewLoc - thisViewLoc;
if (mtDifference < 0) {
mtDifference = mtDifference * -1;
}
if (mtDifference < mtDistance) {
mtDistance = mtDifference;
closesView = thisView;
}
}
}//end class check
}//end loop
//....more stuff
}
http://en.wikipedia.org/wiki/Distance
sqrt(pow((x2 - x1), 2) + pow((y2 - y1), 2)))
Edit: Since you only need to find the shortest distance, not exact distances, you don't need to take the square root. Thanks for the insight Abizern
pow((x2 - x1), 2) + pow((y2 - y1), 2)