NSAttributedString programatically drawned in NSTextField looks different than system drawned in NSTextView - objective-c

This one has been killing me for some time. I have a subclassed NSTextField with:
- (void)drawRect:(NSRect)dirtyRect {
if ([(SFIDisk *)self.cell.representedObject label]) {
// Calculate text width to draw label color around the text.
CGFloat textWidth = self.attributedStringValue.size.width + kSFIDiskViewTitleTextFieldCellMarginForTextSize;
CGFloat textStartX = dirtyRect.origin.x + ((dirtyRect.size.width - textWidth) * .5);
// Draw label background.
[SFIStyleKit drawDiskLabelWithFrame:NSMakeRect(textStartX, dirtyRect.origin.y, textWidth, dirtyRect.size.height)];
}
// Calculate text width to draw label color around the text.
CGFloat textStartY = dirtyRect.origin.y + ((dirtyRect.size.height - self.attributedStringValue.size.height) * .5);
CGFloat textWidth = self.attributedStringValue.size.width;
CGFloat textStartX = dirtyRect.origin.x + ((dirtyRect.size.width - textWidth) * .5);
// Due to the icon in attribtued string, center drawing for string needs to be manually done.
if (!self.currentEditor) {
// Since drawing happens in CALayer and it cannot support font smoothing, this feature needs to be turned off in order to work.
// https://stackoverflow.com/questions/715750/ugly-looking-text-when-drawing-nsattributedstring-in-cgcontext
CGContextSetShouldSmoothFonts((CGContextRef)[[NSGraphicsContext currentContext] graphicsPort], NO);
[self.attributedStringValue drawAtPoint:NSMakePoint(textStartX, textStartY)];
}
}
This seems to works perfect in the UI:
But when editing the NSTextView draws the string a bit heavier:
After editing all goes back to a thinner string:
Goal is to have both looking the same, so am assuming something is wrong in my draw string call since the editor is not manipulated by me.
I cannot figure this out for the life of me. Any ideas?
EDIT:
Did some extra tests by manually setting font for both NSTextField and its corresponding currentEditor when editing.
For every weight set for the font, currentEditor always displays the next weight level.
By finding this out i did a workaround but a proper answer to this is still needed.
Work around code
if ([(SFIDisk *)self.cell.representedObject label]) {
....
self.currentEditor.font = [NSFont systemFontOfSize:13 weight:NSFontWeightLight];
}

While you're editing, you'll be looking at the field editor, which is a NSTextView, rather than your NSTextField. Therefore, your override of -drawRect won't come into play until editing ends and the field editor is hidden again.

Related

Not editable NSTextfiled, to make it auto resize when string is too long, like english word to French

As my question being posted, in my textfield, when I open my app in English, it works well, but when switching to French, we know French word is quite long sometimes and in that case, words showing in the textfield will be cut off.
I tried some customised way for textfield on stack overflow, but it works well for editable textfield, when I made it none-editable, the behaviour will be wired.
It's like when the word is too long, it just make textfield longer, meaning just increase width, not height. What I'm expecting is keeping width fix while changing height when word is too long.
intrinsicSize = [super intrinsicContentSize];
NSTextView *textView = (NSTextView *)fieldEditor;
NSRect usedRect = [textView.textContainer.layoutManager usedRectForTextContainer:textView.textContainer];
kind of two main func I used, when it's editable, everything went well and height changes while width fix, but when it's none editable, only width changes.
Any advice?
In your NSTextField subclass override following methods. Also make sure that NSTextField is set to wrap in IB. Add constraints to it.
-(NSSize)intrinsicContentSize
{
// If wrap is not enabled return the original size
if ( ![self.cell wraps] ) {
return [super intrinsicContentSize];
NSRect frame = [self frame];
CGFloat width = frame.size.width;
// This will allow to grow it in height and not width
frame.size.height = CGFLOAT_MAX;
CGFloat height = [self.cell cellSizeForBounds: frame].height;
// return the calculated height
return NSMakeSize(width, height);
}
// Listen to text change notification and invalidate the default size
- (void)textDidChange:(NSNotification *)notification
{
[super textDidChange:notification];
[self invalidateIntrinsicContentSize];
}

How to find the position of the last text line in a multiline UILabel or otherwise have UILabel have 0 padding

I have a UILabel that has both -numberOfLines set to 3 and text-size auto shrink and I need to align another UIView to this UILabel's last line of text. That is, I might need to align to the y position of line 0, 1 or 2, depending on the text inside the label (and the distance between these lines of text may vary depending on whether the text is long enough that it triggered font resizing).
But:
UILabel doesn't expose a contentSize
the label's bounds extend past the last line of text (there seems to be a content inset), so aligning to the bounds won't work.
subclassing UILabel and doing something like this:
- (void)drawTextInRect:(CGRect)rect {
UIEdgeInsets insets = {0., 0., -30., 0.};
return [super drawTextInRect:UIEdgeInsetsInsetRect(rect, insets)];
}
just happens to work for the case where I have 3 lines and the font size was auto shrunk, but I still can'r figure out a generic way of subtracting insets for the general case, regardless of text size. And I don't seem to be able to use -boundingRectWithSize:options:context: either: it either returns a single line equivalent rect or, If I play around with the options, a a rect the same size of the original label (that is, including the extra insets I'm trying to get rid of). Mind you, the idea behind removing any insets is that if I have no way of knowing where the last line of text is, at least I can remove any insets in the label so that the last line of text aligns with the label's bounds.origin.y + bounds.size.height.
Any thoughts?
I don't know if the problem was that originally I was using boundingRectWithSize on non-attributed text or what but now this seems to work:
NSString *text = <get text from CoreData>;
NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text attributes:#{NSFontAttributeName: self.titleLabel.font}];
CGRect rect = [attributedText boundingRectWithSize:self.titleLabel.frame.size
options:NSStringDrawingUsesLineFragmentOrigin
context:nil];
if (!rect.size.height || rect.size.height > self.titleLabel.frame.size.height) {
attributedText = [[NSAttributedString alloc] initWithString:text attributes:#{NSFontAttributeName: [UIFont boldSystemFontOfSize:self.titleLabel.font.pointSize * self.titleLabel.minimumScaleFactor]}];
rect = [attributedText boundingRectWithSize:self.titleLabel.frame.size
options:NSStringDrawingUsesLineFragmentOrigin
context:nil];
}
self.titleLabel.frame = rect;
self.titleLabel.attributedText = attributedText;
While this doesn't really find the position of the bottom of the last line of text in the UILabel (the label still adds some padding at the bottom... not sure if to account for descenders), it adjusts the label's bounds close enough to the bottom that I can at least align based on bounds.origin.y + bounds.size.height and it looks good enough.

CGRect not completely formed before arranging UI elements

I have the following code
- (void) setTargetGoalFrameToLeftOfWindow: (UIView*) goalView orientation: (UIInterfaceOrientation) orientation {
[goalView setTransform:CGAffineTransformMakeRotation(-M_PI_2)];
CGRect goalFrame = goalView.frame;
CGRect windowFrame = [self getWindowFrame];
CGFloat topMargin;
if (UIInterfaceOrientationIsLandscape(orientation)) {
topMargin = (windowFrame.size.width - goalFrame.size.height) / 2.0;
} else {
topMargin = (windowFrame.size.height - goalFrame.size.height) / 2.0;
}
goalView.frame = CGRectMake(GOAL_MARGIN, topMargin, goalFrame.size.width, goalFrame.size.height);
}
I am creating frame with CGRect and I am using it display my UI elements like labels, buttons etc. I am calculating position based on the window size so that they are in appropriate positions in different orientations. When my app is running, I click on home button. When I open the app again, my UI elements are messed up. They are not in proper positions. The method I mentioned above gets invoked every time I change the orientation and open the app. So, this is getting invoked when I reopen the app. But the problem is that, even before the frame is completely formed, it is taking the width and height at that particular point and calculating positions of my UI elements. This is leading to messed up UI. Is there any way where in I can restrict it to take width and height only after the frame is completely formed? Thanks!

Maintain scroll position in UIWebView when resizing

I have a UIWebView which resizes when the device is rotated. When the webview is resized the scroll position from the top of the page remains the same, but since the height of the content is changing, you end up in a different place at the end of the rotation.
The content of the webview is text and images which are not being scaled to fit. At a smaller width the text breaks more making it taller.
Is there a good way to maintain the scroll position?
One way to kind of work around this problem is to adjust the contentOffset of the webviews's scrollview after a rotation. Due to changes in line breaks in texts the overall size of the page may change during a rotation.
So to solve this, you have to save the old size of the webviews content in the "willRotateToInterfaceOrientation"-method, then calculate the relative change in the "didRotateFromInterfaceOrientation"-method and adjust the scrollviews contentOffset. (That 'almost' solve the problem - i say almost because due to the changes in line breaks, you might end up one or two lines off your desired position.)
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
oldSize = self.webView.scrollView.contentSize;
}
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
CGSize newSize = self.webView.scrollView.contentSize;
float xFactor = newSize.width / oldSize.width;
float yFactor = newSize.height / oldSize.height;
CGPoint newOffset = webView.scrollView.contentOffset;
newOffset.x *= xFactor;
newOffset.y *= yFactor;
webView.scrollView.contentOffset = newOffset;
}
Take a look at my solution: https://github.com/cxa/WebViewContentPositioner. In short, I use document.caretRangeFromPoint(0, 0) to capture the Range, and insert an element to track its position. After resizing, use the captured Range info to decide position.

SetFrame works on iPhone, but not on iPad. Auto resize mask to blame?

I'm trying to resize a UITextView when the keyboard shows. On iPhone it works beautifully. When the the system dispatches a keyboard notification, the text view resizes. When it's done editing, I resize it to fill in the initial space. (Yes, I'm assuming the keyboard is gone when the editing stops. I should change that. However, I don't think that's my issue.)
When I resize the textview on the iPad, the frame resizes correctly, but the app seems to reset the Y value of the frame to zero. Here's my code:
- (void) keyboardDidShowWithNotification:(NSNotification *)aNotification{
//
// If the content view being edited
// then show shrink it to fit above the keyboard.
//
if ([self.contentTextView isFirstResponder]) {
//
// Grab the keyboard size "meta data"
//
NSDictionary *info = [aNotification userInfo];
CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
//
// Calculate the amount of the view that the keyboard hides.
//
// Here we do some confusing math voodoo.
//
// Get the bottom of the screen, subtract that
// from the keyboard height, then take the
// difference and set that as the bottom inset
// of the content text view.
//
float screenHeightMinusBottom = self.contentTextView.frame.size.height + self.contentTextView.frame.origin.y;
float heightOfBottom = self.view.frame.size.height - screenHeightMinusBottom;
float insetAmount = kbSize.height - heightOfBottom;
//
// Don't stretch the text to reach the keyboard if it's shorter.
//
if (insetAmount < 0) {
return;
}
self.keyboardOverlapPortrait = insetAmount;
float initialOriginX = self.contentTextView.frame.origin.x;
float initialOriginY = self.contentTextView.frame.origin.y;
[self.contentTextView setFrame:CGRectMake(initialOriginX, initialOriginY, self.contentTextView.frame.size.width, self.contentTextView.frame.size.height-insetAmount)];
}
Why would this work on iPhone, and not work on iPad? Also, can my autoresize masks be making an unexpected change?
Like said #bandejapaisa, I found that the orientation was a problem, at least during my tests.
The first thing, is about the use of kbSize.height being misleading, because in Landscape orientation it represents the width of the keyboard. So, as your code is in a UIViewController you can use it this way:
float insetAmount = (UIInterfaceOrientationIsPortrait(self.interfaceOrientation)?kbSize.height:kbSize.width) - heightOfBottom;
The self.interfaceOrientation gives the orientation of the Interface (can be different from the Device orientation) and the macro UIInterfaceOrientationIsPortrait returns YES if the given orientation is Portrait (top or bottom). So as the keyboard height is in the kbSize.height when the interface is Portrait, and in the kbSize.width when the interface is Landscape, we simply need to test the orientation to get the good value.
But that's not enough, cause I've discovered the same problem with the self.view.frame.size.height value. So I used the same workaround:
float heightOfBottom = (UIInterfaceOrientationIsPortrait(self.interfaceOrientation)?self.view.frame.size.height:self.view.frame.size.width) - screenHeightMinusBottom;
Hope this helps...