| // |
| // GTMUILocalizerAndLayoutTweaker.m |
| // |
| // Copyright 2009 Google Inc. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| // use this file except in compliance with the License. You may obtain a copy |
| // of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| // License for the specific language governing permissions and limitations under |
| // the License. |
| // |
| |
| #import "GTMUILocalizerAndLayoutTweaker.h" |
| #import "GTMUILocalizer.h" |
| |
| // Controls if +wrapString:width:font: uses a subclassed TypeSetter to do |
| // its work in one pass. |
| #define GTM_USE_TYPESETTER 1 |
| |
| // Helper that will try to do a SizeToFit on any UI items and do the special |
| // case handling we also need to end up with a usable UI item. It also takes |
| // an offset so we can slide the item if we need to. |
| // Returns the change in the view's size. |
| static NSSize SizeToFit(NSView *view, NSPoint offset); |
| // Compare function for -[NSArray sortedArrayUsingFunction:context:] |
| static NSInteger CompareFrameX(id view1, id view2, void *context); |
| // Check if the view is anchored on the right (fixed right, flexible left). |
| static BOOL IsRightAnchored(NSView *view); |
| |
| // Constant for a forced string wrap in button cells (Opt-Return in IB inserts |
| // this into the string). |
| NSString * const kForcedWrapString = @"\xA"; |
| // Radio and Checkboxes (NSButtonCell) appears to use two different layout |
| // algorithms for sizeToFit calls and drawing calls when there is a forced word |
| // wrap in the title. The result is a sizeToFit can tell you it all fits N |
| // lines in the given rect, but at draw time, it draws as >N lines and never |
| // gets as wide, resulting in a clipped control. This fudge factor is what is |
| // added to try and avoid these by giving the size calls just enough slop to |
| // handle the differences. |
| // radar://7831901 different wrapping between sizeToFit and drawing |
| static const CGFloat kWrapperStringSlop = 0.9; |
| |
| #if GTM_USE_TYPESETTER |
| |
| @interface GTMBreakRecordingTypeSetter : NSATSTypesetter { |
| @private |
| NSMutableArray *array_; |
| } |
| @end |
| |
| @implementation GTMBreakRecordingTypeSetter |
| - (id)init { |
| if ((self = [super init])) { |
| array_ = [[NSMutableArray alloc] init]; |
| } |
| return self; |
| } |
| |
| - (void)dealloc { |
| [array_ release]; |
| [super dealloc]; |
| } |
| |
| - (BOOL)shouldBreakLineByWordBeforeCharacterAtIndex:(NSUInteger)charIndex { |
| [array_ addObject:[NSNumber numberWithUnsignedInteger:charIndex]]; |
| return YES; |
| } |
| |
| - (NSArray*)breakArray { |
| return array_; |
| } |
| |
| @end |
| |
| #endif // GTM_USE_TYPESETTER |
| |
| @interface GTMUILocalizerAndLayoutTweaker (PrivateMethods) |
| // Recursively walk the UI triggering Tweakers. |
| - (void)tweakView:(NSView *)view; |
| // Insert newlines so the string wraps to the given width using the requested |
| // font. |
| + (NSString*)wrapString:(NSString *)string |
| width:(CGFloat)width |
| font:(NSFont *)font; |
| @end |
| |
| @interface GTMWidthBasedTweaker (InternalMethods) |
| // Does the actual work to size and adjust the views within this Tweaker. The |
| // offset is the amount this view should shift as part of it's resize. |
| // Returns change in this view's width. |
| - (CGFloat)tweakLayoutWithOffset:(NSPoint)offset; |
| @end |
| |
| @implementation GTMUILocalizerAndLayoutTweaker |
| |
| - (void)awakeFromNib { |
| if (uiObject_) { |
| GTMUILocalizer *localizer = localizer_; |
| if (!localizer) { |
| NSBundle *bundle = [GTMUILocalizer bundleForOwner:localizerOwner_]; |
| localizer = [[[GTMUILocalizer alloc] initWithBundle:bundle] autorelease]; |
| } |
| [self applyLocalizer:localizer tweakingUI:uiObject_]; |
| } |
| } |
| |
| - (void)applyLocalizer:(GTMUILocalizer *)localizer |
| tweakingUI:(id)uiObject { |
| // Localize first |
| [localizer localizeObject:uiObject recursively:YES]; |
| |
| // Then tweak! |
| [self tweakUI:uiObject]; |
| } |
| |
| - (void)tweakUI:(id)uiObject { |
| // Figure out where we start |
| NSView *startView; |
| if ([uiObject isKindOfClass:[NSWindow class]]) { |
| startView = [(NSWindow *)uiObject contentView]; |
| } else { |
| _GTMDevAssert([uiObject isKindOfClass:[NSView class]], |
| @"should have been a subclass of NSView"); |
| startView = (NSView *)uiObject; |
| } |
| |
| // Tweak away! |
| [self tweakView:startView]; |
| } |
| |
| - (void)tweakView:(NSView *)view { |
| // If it's a alignment box, let it do its thing... |
| if ([view isKindOfClass:[GTMWidthBasedTweaker class]]) { |
| [(GTMWidthBasedTweaker *)view tweakLayoutWithOffset:NSZeroPoint]; |
| // Do our best to support TabViews. If the tabs need to resize, you are |
| // probably better off manually running them through a tweaker and then fixing |
| // up the parent view (and other tabs) to look right. |
| } else if ([view isKindOfClass:[NSTabView class]]) { |
| NSArray *tabViewItems = [(NSTabView *)view tabViewItems]; |
| NSTabViewItem *item = nil; |
| for (item in tabViewItems) { |
| [self tweakView:[item view]]; |
| } |
| // Generically look for subviews... |
| } else { |
| NSArray *subviews = [view subviews]; |
| NSView *subview = nil; |
| for (subview in subviews) { |
| [self tweakView:subview]; |
| } |
| } |
| } |
| |
| + (NSString*)wrapString:(NSString *)string |
| width:(CGFloat)width |
| font:(NSFont *)font { |
| // Set up the objects needed for the layout work. |
| NSRect targetRect = NSMakeRect(0, 0, width, CGFLOAT_MAX); |
| NSTextContainer* textContainer = |
| [[[NSTextContainer alloc] initWithContainerSize:targetRect.size] |
| autorelease]; |
| NSLayoutManager* layoutManager = [[[NSLayoutManager alloc] init] autorelease]; |
| NSTextStorage* textStorage = |
| [[[NSTextStorage alloc] initWithString:string] autorelease]; |
| |
| [textStorage addLayoutManager:layoutManager]; |
| [layoutManager addTextContainer:textContainer]; |
| // From playing in interface builder, the padding seems to be 2 on the line |
| // fragments to get the same wrapping as what the NSCell will do in the end. |
| [textContainer setLineFragmentPadding:2.0f]; |
| |
| if (font == NULL) { |
| // If the font is NULL, it is the System font. |
| font = [NSFont systemFontOfSize:[NSFont systemFontSize]]; |
| } |
| // Apply the font. |
| [textStorage setFont:font]; |
| |
| // Get the mutable string for the layout, remove any forced wraps in it. |
| NSMutableString* workerStr = [textStorage mutableString]; |
| [workerStr replaceOccurrencesOfString:kForcedWrapString |
| withString:@"" |
| options:NSLiteralSearch |
| range:NSMakeRange(0, [workerStr length])]; |
| |
| #if GTM_USE_TYPESETTER |
| // Put in the recording type setter. |
| GTMBreakRecordingTypeSetter *typeSetter = |
| [[[GTMBreakRecordingTypeSetter alloc] init] autorelease]; |
| [layoutManager setTypesetter:typeSetter]; |
| // Make sure things are layed out (10.5 has a clean API for this, 10.4 |
| // doesn't). |
| #if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 |
| [layoutManager ensureLayoutForCharacterRange:NSMakeRange(0, |
| [textStorage length])]; |
| #else |
| [layoutManager lineFragmentRectForGlyphAtIndex:[layoutManager numberOfGlyphs]-1 |
| effectiveRange:NULL]; |
| #endif |
| |
| // Insert the breaks everywere the type setter got asked about breaks. |
| NSEnumerator *reverseEnumerator = |
| [[typeSetter breakArray] reverseObjectEnumerator]; |
| NSNumber *number; |
| while ((number = [reverseEnumerator nextObject]) != nil) { |
| [workerStr insertString:kForcedWrapString |
| atIndex:[number unsignedIntegerValue]]; |
| } |
| #else |
| // Find out how tall lines would be for the layout loop. |
| CGFloat lineHeight = [layoutManager defaultLineHeightForFont:font]; |
| targetRect.size.height = lineHeight; |
| |
| // Loop until all glyphs are layout out. |
| NSUInteger numGlyphsUsed = 0; |
| while (numGlyphsUsed < [layoutManager numberOfGlyphs]) { |
| // See what fits in the current rect |
| NSRange range = [layoutManager glyphRangeForBoundingRect:targetRect |
| inTextContainer:textContainer]; |
| numGlyphsUsed = NSMaxRange(range); |
| if (numGlyphsUsed < [layoutManager numberOfGlyphs]) { |
| // Didn't all fit, add a break, and grow the rect to try again. |
| NSRange charRange = [layoutManager glyphRangeForCharacterRange:range |
| actualCharacterRange:nil]; |
| [workerStr insertString:kForcedWrapString atIndex:NSMaxRange(charRange)]; |
| targetRect.size.height += lineHeight; |
| } |
| } |
| #endif // GTM_USE_TYPESETTER |
| |
| // Return the string with forced wraps |
| return [[workerStr copy] autorelease]; |
| } |
| |
| + (NSSize)sizeToFitView:(NSView *)view { |
| return SizeToFit(view, NSZeroPoint); |
| } |
| |
| + (CGFloat)sizeToFitFixedWidthTextField:(NSTextField *)textField { |
| NSRect initialFrame = [textField frame]; |
| NSRect sizeRect = NSMakeRect(0, 0, NSWidth(initialFrame), CGFLOAT_MAX); |
| NSSize newSize = [[textField cell] cellSizeForBounds:sizeRect]; |
| newSize.width = NSWidth(initialFrame); |
| [textField setFrameSize:newSize]; |
| return newSize.height - NSHeight(initialFrame); |
| } |
| |
| #if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 |
| |
| + (CGFloat)sizeToFitFixedHeightTextField:(NSTextField *)textField { |
| return [self sizeToFitFixedHeightTextField:textField minWidth:(CGFloat)0]; |
| } |
| |
| + (CGFloat)sizeToFitFixedHeightTextField:(NSTextField *)textField |
| minWidth:(NSUInteger)minWidth { |
| NSRect initialRect = [textField frame]; |
| NSCell *cell = [textField cell]; |
| NSSize titleSize = [cell titleRectForBounds:initialRect].size; |
| |
| // Find linebreak point, and keep trying them until we're under the height |
| // requested. |
| NSString *str = [textField stringValue]; |
| CFStringTokenizerRef tokenizer = |
| CFStringTokenizerCreate(NULL, |
| (CFStringRef)str, |
| CFRangeMake(0, [str length]), |
| kCFStringTokenizerUnitLineBreak, |
| NULL); |
| if (!tokenizer) { |
| _GTMDevAssert(tokenizer, @"failed to get a tokenizer"); |
| return 0.0; |
| } |
| NSCell *workerCell = [[cell copy] autorelease]; |
| |
| // Loop trying line break points until the height fits. |
| while (1) { |
| CFStringTokenizerTokenType tokenType = |
| CFStringTokenizerAdvanceToNextToken(tokenizer); |
| if (tokenType == kCFStringTokenizerTokenNone) { |
| // Reached the end without ever find a good width, how? |
| _GTMDevAssert(0, @"Failed to find a good size?!"); |
| [textField sizeToFit]; |
| break; |
| } |
| CFRange tokenRange = CFStringTokenizerGetCurrentTokenRange(tokenizer); |
| |
| NSRange subStringRange = |
| NSMakeRange(0, tokenRange.location + tokenRange.length); |
| NSString *subString = [str substringWithRange:subStringRange]; |
| |
| // Find how wide the cell would be for this sub string. |
| [workerCell setStringValue:subString]; |
| CGFloat testWidth = [workerCell cellSize].width; |
| |
| // Find the overall size if wrapped to this width. |
| NSRect sizeRect = NSMakeRect(0, 0, testWidth, CGFLOAT_MAX); |
| NSSize newSize = [cell cellSizeForBounds:sizeRect]; |
| if (newSize.height <= titleSize.height) { |
| [textField setFrameSize:newSize]; |
| break; |
| } |
| } |
| |
| CFRelease(tokenizer); |
| |
| NSSize fixedSize = [textField frame].size; |
| NSSize finalSize = NSMakeSize(fixedSize.width, NSHeight(initialRect)); |
| |
| // Enforce the minWidth |
| if (minWidth > fixedSize.width) { |
| finalSize.width = minWidth; |
| } |
| // Make integral. |
| finalSize.width = ceil(fixedSize.width); |
| finalSize.height = ceil(fixedSize.height); |
| if (!NSEqualSizes(fixedSize, finalSize)) { |
| [textField setFrameSize:finalSize]; |
| } |
| |
| // Return how much things changed |
| return finalSize.width - NSWidth(initialRect); |
| } |
| |
| #endif // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 |
| |
| + (void)wrapButtonTitleForWidth:(NSButton *)button { |
| NSCell *cell = [button cell]; |
| NSRect frame = [button frame]; |
| |
| NSRect titleFrame = [cell titleRectForBounds:frame]; |
| |
| NSString* newTitle = [self wrapString:[button title] |
| width:NSWidth(titleFrame) |
| font:[button font]]; |
| [button setTitle:newTitle]; |
| } |
| |
| + (void)wrapRadioGroupForWidth:(NSMatrix *)radioGroup { |
| NSSize cellSize = [radioGroup cellSize]; |
| NSRect tmpRect = NSMakeRect(0, 0, cellSize.width, cellSize.height); |
| NSFont *font = [radioGroup font]; |
| |
| NSCell *cell; |
| for (cell in [radioGroup cells]) { |
| NSRect titleFrame = [cell titleRectForBounds:tmpRect]; |
| NSString* newTitle = [self wrapString:[cell title] |
| width:NSWidth(titleFrame) |
| font:font]; |
| [cell setTitle:newTitle]; |
| } |
| } |
| |
| + (void)resizeWindowWithoutAutoResizingSubViews:(NSWindow*)window |
| delta:(NSSize)delta { |
| NSView *contentView = [window contentView]; |
| |
| // Clear autosizesSubviews (saving the state). |
| BOOL autoresizesSubviews = [contentView autoresizesSubviews]; |
| if (autoresizesSubviews) { |
| [contentView setAutoresizesSubviews:NO]; |
| } |
| |
| NSRect rect = [contentView convertRect:[window frame] fromView:nil]; |
| rect.size.width += delta.width; |
| rect.size.height += delta.height; |
| rect = [contentView convertRect:rect toView:nil]; |
| [window setFrame:rect display:NO]; |
| // For some reason the content view is resizing, but some times not adjusting |
| // its origin, so correct it manually. |
| [contentView setFrameOrigin:NSMakePoint(0, 0)]; |
| |
| // Restore autosizesSubviews. |
| if (autoresizesSubviews) { |
| [contentView setAutoresizesSubviews:YES]; |
| } |
| } |
| |
| + (void)resizeViewWithoutAutoResizingSubViews:(NSView*)view |
| delta:(NSSize)delta { |
| // Clear autosizesSubviews (saving the state). |
| BOOL autoresizesSubviews = [view autoresizesSubviews]; |
| if (autoresizesSubviews) { |
| [view setAutoresizesSubviews:NO]; |
| } |
| |
| NSRect rect = [view frame]; |
| rect.size.width += delta.width; |
| rect.size.height += delta.height; |
| [view setFrame:rect]; |
| |
| // Restore autosizesSubviews. |
| if (autoresizesSubviews) { |
| [view setAutoresizesSubviews:YES]; |
| } |
| } |
| |
| @end |
| |
| @implementation GTMWidthBasedTweaker |
| |
| - (CGFloat)changedWidth { |
| return widthChange_; |
| } |
| |
| - (CGFloat)tweakLayoutWithOffset:(NSPoint)offset { |
| NSArray *subviews = [self subviews]; |
| if (![subviews count]) { |
| widthChange_ = 0.0; |
| return widthChange_; |
| } |
| |
| BOOL sumMode = NO; |
| NSMutableArray *rightAlignedSubViews = nil; |
| NSMutableArray *rightAlignedSubViewDeltas = nil; |
| if ([subviews count] > 1) { |
| // Check if the frames are in a row by seeing if when they are left aligned |
| // they overlap. If they don't overlap in this case, it means they are |
| // probably stacked instead. |
| NSRect rect1 = [[subviews objectAtIndex:0] frame]; |
| NSRect rect2 = [[subviews objectAtIndex:1] frame]; |
| rect1.origin.x = rect2.origin.x = 0; |
| if (NSIntersectsRect(rect1, rect2)) { |
| // No, so walk them x order moving them along so they don't overlap. |
| sumMode = YES; |
| subviews = [subviews sortedArrayUsingFunction:CompareFrameX context:NULL]; |
| } else { |
| // Since they are vertical, any views pinned to the right will have to be |
| // shifted after we finish figuring out the final size. |
| rightAlignedSubViews = [NSMutableArray array]; |
| rightAlignedSubViewDeltas = [NSMutableArray array]; |
| } |
| } |
| |
| // Size our subviews |
| NSView *subView; |
| CGFloat finalDelta = sumMode ? 0 : -CGFLOAT_MAX; |
| NSPoint subViewOffset = NSZeroPoint; |
| for (subView in subviews) { |
| if (sumMode) { |
| subViewOffset.x = finalDelta; |
| } |
| CGFloat delta = SizeToFit(subView, subViewOffset).width; |
| if (sumMode) { |
| finalDelta += delta; |
| } else { |
| if (delta > finalDelta) { |
| finalDelta = delta; |
| } |
| } |
| // Track the right anchored subviews size changes so we can update them |
| // once we know this view's size. |
| if (IsRightAnchored(subView)) { |
| [rightAlignedSubViews addObject:subView]; |
| #if CGFLOAT_IS_DOUBLE |
| NSNumber *nsDelta = [NSNumber numberWithDouble:delta]; |
| #else |
| NSNumber *nsDelta = [NSNumber numberWithFloat:delta]; |
| #endif |
| [rightAlignedSubViewDeltas addObject:nsDelta]; |
| } |
| } |
| |
| // Are we pinned to the right of our parent? |
| BOOL rightAnchored = IsRightAnchored(self); |
| |
| // Adjust our size (turn off auto resize, because we just fixed up all the |
| // objects within us). |
| BOOL autoresizesSubviews = [self autoresizesSubviews]; |
| if (autoresizesSubviews) { |
| [self setAutoresizesSubviews:NO]; |
| } |
| NSRect selfFrame = [self frame]; |
| selfFrame.size.width += finalDelta; |
| if (rightAnchored) { |
| // Right side is anchored, so we need to slide back to the left. |
| selfFrame.origin.x -= finalDelta; |
| } |
| selfFrame.origin.x += offset.x; |
| selfFrame.origin.y += offset.y; |
| [self setFrame:selfFrame]; |
| if (autoresizesSubviews) { |
| [self setAutoresizesSubviews:autoresizesSubviews]; |
| } |
| |
| // Now spin over the list of right aligned view and their size changes |
| // fixing up their positions so they are still right aligned in our final |
| // view. |
| for (NSUInteger lp = 0; lp < [rightAlignedSubViews count]; ++lp) { |
| subView = [rightAlignedSubViews objectAtIndex:lp]; |
| CGFloat delta = [[rightAlignedSubViewDeltas objectAtIndex:lp] doubleValue]; |
| NSRect viewFrame = [subView frame]; |
| viewFrame.origin.x += -delta + finalDelta; |
| [subView setFrame:viewFrame]; |
| } |
| |
| if (viewToSlideAndResize_) { |
| NSRect viewFrame = [viewToSlideAndResize_ frame]; |
| if (!rightAnchored) { |
| // If our right wasn't anchored, this view slides (we push it right). |
| // (If our right was anchored, the assumption is the view is in front of |
| // us so its x shouldn't move.) |
| viewFrame.origin.x += finalDelta; |
| } |
| viewFrame.size.width -= finalDelta; |
| [viewToSlideAndResize_ setFrame:viewFrame]; |
| } |
| if (viewToSlide_) { |
| NSRect viewFrame = [viewToSlide_ frame]; |
| // Move the view the same direction we moved. |
| if (rightAnchored) { |
| viewFrame.origin.x -= finalDelta; |
| } else { |
| viewFrame.origin.x += finalDelta; |
| } |
| [viewToSlide_ setFrame:viewFrame]; |
| } |
| if (viewToResize_) { |
| if ([viewToResize_ isKindOfClass:[NSWindow class]]) { |
| NSWindow *window = (NSWindow *)viewToResize_; |
| NSView *contentView = [window contentView]; |
| NSRect windowFrame = [contentView convertRect:[window frame] |
| fromView:nil]; |
| windowFrame.size.width += finalDelta; |
| windowFrame = [contentView convertRect:windowFrame toView:nil]; |
| [window setFrame:windowFrame display:YES]; |
| // For some reason the content view is resizing, but not adjusting its |
| // origin, so correct it manually. |
| [contentView setFrameOrigin:NSMakePoint(0, 0)]; |
| // TODO: should we update min size? |
| } else { |
| NSRect viewFrame = [viewToResize_ frame]; |
| viewFrame.size.width += finalDelta; |
| [viewToResize_ setFrame:viewFrame]; |
| // TODO: should we check if this view is right anchored, and adjust its |
| // x position also? |
| } |
| } |
| |
| widthChange_ = finalDelta; |
| return widthChange_; |
| } |
| |
| @end |
| |
| #pragma mark - |
| |
| static NSSize SizeToFit(NSView *view, NSPoint offset) { |
| |
| // If we've got one of us within us, recurse (for grids) |
| if ([view isKindOfClass:[GTMWidthBasedTweaker class]]) { |
| GTMWidthBasedTweaker *widthAlignmentBox = (GTMWidthBasedTweaker *)view; |
| return NSMakeSize([widthAlignmentBox tweakLayoutWithOffset:offset], 0); |
| } |
| |
| NSRect oldFrame = [view frame]; |
| NSRect fitFrame = oldFrame; |
| NSRect newFrame = oldFrame; |
| |
| if ([view isKindOfClass:[NSTextField class]] && |
| [(NSTextField *)view isEditable]) { |
| // Don't try to sizeToFit because edit fields really don't want to be sized |
| // to what is in them as they are for users to enter things so honor their |
| // current size. |
| #if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 |
| } else if ([view isKindOfClass:[NSPathControl class]]) { |
| // Don't try to sizeToFit because NSPathControls usually need to be able |
| // to display any path, so they shouldn't tight down to whatever they |
| // happen to be listing at the moment. |
| #endif // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5 |
| } else { |
| // Genericaly fire a sizeToFit if it has one. |
| if ([view respondsToSelector:@selector(sizeToFit)]) { |
| [view performSelector:@selector(sizeToFit)]; |
| fitFrame = [view frame]; |
| newFrame = fitFrame; |
| |
| if ([view isKindOfClass:[NSMatrix class]]) { |
| NSMatrix *matrix = (NSMatrix *)view; |
| // See note on kWrapperStringSlop for why this is done. |
| NSCell *cell; |
| for (cell in [matrix cells]) { |
| if ([[cell title] rangeOfString:kForcedWrapString].location != |
| NSNotFound) { |
| newFrame.size.width += kWrapperStringSlop; |
| break; |
| } |
| } |
| } |
| |
| } |
| |
| if ([view isKindOfClass:[NSButton class]]) { |
| NSButton *button = (NSButton *)view; |
| // -[NSButton sizeToFit] gives much worse results than IB's Size to Fit |
| // option for standard push buttons. |
| if (([button bezelStyle] == NSRoundedBezelStyle) && |
| ([[button cell] controlSize] == NSRegularControlSize)) { |
| // This is the amount of padding IB adds over a sizeToFit, empirically |
| // determined. |
| const CGFloat kExtraPaddingAmount = 12.0; |
| // Width is tricky, new buttons in IB are 96 wide, Carbon seems to have |
| // defaulted to 70, Cocoa seems to like 82. But we go with 96 since |
| // that's what IB is doing these days. |
| const CGFloat kMinButtonWidth = (CGFloat)96.0; |
| newFrame.size.width = NSWidth(newFrame) + kExtraPaddingAmount; |
| if (NSWidth(newFrame) < kMinButtonWidth) { |
| newFrame.size.width = kMinButtonWidth; |
| } |
| } else if ([button bezelStyle] == NSTexturedRoundedBezelStyle && |
| [[button cell] controlSize] == NSRegularControlSize) { |
| // The round textured style needs to have a little extra padding, |
| // otherwise the baseline of the text sinks by a few pixels. |
| const CGFloat kExtraPaddingAmount = 4.0; |
| newFrame.size.width += kExtraPaddingAmount; |
| } else { |
| // See note on kWrapperStringSlop for why this is done. |
| NSString *title = [button title]; |
| if ([title rangeOfString:kForcedWrapString].location != NSNotFound) { |
| newFrame.size.width += kWrapperStringSlop; |
| } |
| } |
| } |
| } |
| |
| // Apply the offset, and see if we need to change the frame (again). |
| newFrame.origin.x += offset.x; |
| newFrame.origin.y += offset.y; |
| if (!NSEqualRects(fitFrame, newFrame)) { |
| [view setFrame:newFrame]; |
| } |
| |
| // Return how much we changed size. |
| return NSMakeSize(NSWidth(newFrame) - NSWidth(oldFrame), |
| NSHeight(newFrame) - NSHeight(oldFrame)); |
| } |
| |
| static NSInteger CompareFrameX(id view1, id view2, void *context) { |
| CGFloat x1 = [view1 frame].origin.x; |
| CGFloat x2 = [view2 frame].origin.x; |
| if (x1 < x2) |
| return NSOrderedAscending; |
| else if (x1 > x2) |
| return NSOrderedDescending; |
| else |
| return NSOrderedSame; |
| } |
| |
| static BOOL IsRightAnchored(NSView *view) { |
| NSUInteger autoresizing = [view autoresizingMask]; |
| BOOL viewRightAnchored = |
| ((autoresizing & (NSViewMinXMargin | NSViewMaxXMargin)) == NSViewMinXMargin); |
| return viewRightAnchored; |
| } |