PageRenderTime 115ms CodeModel.GetById 9ms app.highlight 99ms RepoModel.GetById 2ms app.codeStats 0ms

/core/externals/google-toolbox-for-mac/AppKit/GTMUILocalizerAndLayoutTweaker.m

http://macfuse.googlecode.com/
Objective C | 655 lines | 450 code | 80 blank | 125 comment | 70 complexity | 1a94270b2e1fee7b840d0033ef5e88d7 MD5 | raw file
  1//
  2//  GTMUILocalizerAndLayoutTweaker.m
  3//
  4//  Copyright 2009 Google Inc.
  5//
  6//  Licensed under the Apache License, Version 2.0 (the "License"); you may not
  7//  use this file except in compliance with the License.  You may obtain a copy
  8//  of the License at
  9//
 10//  http://www.apache.org/licenses/LICENSE-2.0
 11//
 12//  Unless required by applicable law or agreed to in writing, software
 13//  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 14//  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 15//  License for the specific language governing permissions and limitations under
 16//  the License.
 17//
 18
 19#import "GTMUILocalizerAndLayoutTweaker.h"
 20#import "GTMUILocalizer.h"
 21
 22// Controls if +wrapString:width:font: uses a subclassed TypeSetter to do
 23// its work in one pass.
 24#define GTM_USE_TYPESETTER 1
 25
 26// Helper that will try to do a SizeToFit on any UI items and do the special
 27// case handling we also need to end up with a usable UI item.  It also takes
 28// an offset so we can slide the item if we need to.
 29// Returns the change in the view's size.
 30static NSSize SizeToFit(NSView *view, NSPoint offset);
 31// Compare function for -[NSArray sortedArrayUsingFunction:context:]
 32static NSInteger CompareFrameX(id view1, id view2, void *context);
 33// Check if the view is anchored on the right (fixed right, flexible left).
 34static BOOL IsRightAnchored(NSView *view);
 35
 36// Constant for a forced string wrap in button cells (Opt-Return in IB inserts
 37// this into the string).
 38NSString * const kForcedWrapString = @"\xA";
 39// Radio and Checkboxes (NSButtonCell) appears to use two different layout
 40// algorithms for sizeToFit calls and drawing calls when there is a forced word
 41// wrap in the title.  The result is a sizeToFit can tell you it all fits N
 42// lines in the given rect, but at draw time, it draws as >N lines and never
 43// gets as wide, resulting in a clipped control.  This fudge factor is what is
 44// added to try and avoid these by giving the size calls just enough slop to
 45// handle the differences.
 46// radar://7831901 different wrapping between sizeToFit and drawing
 47static const CGFloat kWrapperStringSlop = 0.9;
 48
 49#if GTM_USE_TYPESETTER
 50
 51@interface GTMBreakRecordingTypeSetter : NSATSTypesetter {
 52 @private
 53  NSMutableArray *array_;
 54}
 55@end
 56
 57@implementation GTMBreakRecordingTypeSetter
 58- (id)init {
 59  if ((self = [super init])) {
 60    array_ = [[NSMutableArray alloc] init];
 61  }
 62  return self;
 63}
 64
 65- (void)dealloc {
 66  [array_ release];
 67  [super dealloc];
 68}
 69
 70- (BOOL)shouldBreakLineByWordBeforeCharacterAtIndex:(NSUInteger)charIndex {
 71  [array_ addObject:[NSNumber numberWithUnsignedInteger:charIndex]];
 72  return YES;
 73}
 74
 75- (NSArray*)breakArray {
 76  return array_;
 77}
 78
 79@end
 80
 81#endif  // GTM_USE_TYPESETTER
 82
 83@interface GTMUILocalizerAndLayoutTweaker (PrivateMethods)
 84// Recursively walk the UI triggering Tweakers.
 85- (void)tweakView:(NSView *)view;
 86// Insert newlines so the string wraps to the given width using the requested
 87// font.
 88+ (NSString*)wrapString:(NSString *)string
 89                  width:(CGFloat)width
 90                   font:(NSFont *)font;
 91@end
 92
 93@interface GTMWidthBasedTweaker (InternalMethods)
 94// Does the actual work to size and adjust the views within this Tweaker.  The
 95// offset is the amount this view should shift as part of it's resize.
 96// Returns change in this view's width.
 97- (CGFloat)tweakLayoutWithOffset:(NSPoint)offset;
 98@end
 99
100@implementation GTMUILocalizerAndLayoutTweaker
101
102- (void)awakeFromNib {
103  if (uiObject_) {
104    GTMUILocalizer *localizer = localizer_;
105    if (!localizer) {
106      NSBundle *bundle = [GTMUILocalizer bundleForOwner:localizerOwner_];
107      localizer = [[[GTMUILocalizer alloc] initWithBundle:bundle] autorelease];
108    }
109    [self applyLocalizer:localizer tweakingUI:uiObject_];
110  }
111}
112
113- (void)applyLocalizer:(GTMUILocalizer *)localizer
114            tweakingUI:(id)uiObject {
115  // Localize first
116  [localizer localizeObject:uiObject recursively:YES];
117
118  // Then tweak!
119  [self tweakUI:uiObject];
120}
121
122- (void)tweakUI:(id)uiObject {
123  // Figure out where we start
124  NSView *startView;
125  if ([uiObject isKindOfClass:[NSWindow class]]) {
126    startView = [(NSWindow *)uiObject contentView];
127  } else {
128    _GTMDevAssert([uiObject isKindOfClass:[NSView class]],
129                  @"should have been a subclass of NSView");
130    startView = (NSView *)uiObject;
131  }
132
133  // Tweak away!
134  [self tweakView:startView];
135}
136
137- (void)tweakView:(NSView *)view {
138  // If it's a alignment box, let it do its thing...
139  if ([view isKindOfClass:[GTMWidthBasedTweaker class]]) {
140    [(GTMWidthBasedTweaker *)view tweakLayoutWithOffset:NSZeroPoint];
141  // Do our best to support TabViews. If the tabs need to resize, you are
142  // probably better off manually running them through a tweaker and then fixing
143  // up the parent view (and other tabs) to look right.
144  } else if ([view isKindOfClass:[NSTabView class]]) {
145    NSArray *tabViewItems = [(NSTabView *)view tabViewItems];
146    NSTabViewItem *item = nil;
147    GTM_FOREACH_OBJECT(item, tabViewItems) {
148      [self tweakView:[item view]];
149    }
150  // Generically look for subviews...
151  } else {
152    NSArray *subviews = [view subviews];
153    NSView *subview = nil;
154    GTM_FOREACH_OBJECT(subview, subviews) {
155      [self tweakView:subview];
156    }
157  }
158}
159
160+ (NSString*)wrapString:(NSString *)string
161                  width:(CGFloat)width
162                   font:(NSFont *)font {
163  // Set up the objects needed for the layout work.
164  NSRect targetRect = NSMakeRect(0, 0, width, CGFLOAT_MAX);
165  NSTextContainer* textContainer =
166    [[[NSTextContainer alloc] initWithContainerSize:targetRect.size]
167     autorelease];
168  NSLayoutManager* layoutManager = [[[NSLayoutManager alloc] init] autorelease];
169  NSTextStorage* textStorage =
170    [[[NSTextStorage alloc] initWithString:string] autorelease];
171
172  [textStorage addLayoutManager:layoutManager];
173  [layoutManager addTextContainer:textContainer];
174  // From playing in interface builder, the padding seems to be 2 on the line
175  // fragments to get the same wrapping as what the NSCell will do in the end.
176  [textContainer setLineFragmentPadding:2.0f];
177
178  // Apply the font.
179  [textStorage setFont:font];
180
181  // Get the mutable string for the layout, remove any forced wraps in it.
182  NSMutableString* workerStr = [textStorage mutableString];
183  [workerStr replaceOccurrencesOfString:kForcedWrapString
184                             withString:@""
185                                options:NSLiteralSearch
186                                  range:NSMakeRange(0, [workerStr length])];
187
188#if GTM_USE_TYPESETTER
189  // Put in the recording type setter.
190  GTMBreakRecordingTypeSetter *typeSetter =
191    [[[GTMBreakRecordingTypeSetter alloc] init] autorelease];
192  [layoutManager setTypesetter:typeSetter];
193  // Make sure things are layed out (10.5 has a clean API for this, 10.4
194  // doesn't).
195#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5
196  [layoutManager ensureLayoutForCharacterRange:NSMakeRange(0,
197                                                           [textStorage length])];
198#else
199  [layoutManager lineFragmentRectForGlyphAtIndex:[layoutManager numberOfGlyphs]-1
200                                  effectiveRange:NULL];
201#endif
202
203  // Insert the breaks everywere the type setter got asked about breaks.
204  NSEnumerator *reverseEnumerator =
205    [[typeSetter breakArray] reverseObjectEnumerator];
206  NSNumber *number;
207  while ((number = [reverseEnumerator nextObject]) != nil) {
208    [workerStr insertString:kForcedWrapString
209                    atIndex:[number unsignedIntegerValue]];
210  }
211#else
212  // Find out how tall lines would be for the layout loop.
213  CGFloat lineHeight = [layoutManager defaultLineHeightForFont:font];
214  targetRect.size.height = lineHeight;
215
216  // Loop until all glyphs are layout out.
217  NSUInteger numGlyphsUsed = 0;
218  while (numGlyphsUsed < [layoutManager numberOfGlyphs]) {
219    // See what fits in the current rect
220    NSRange range = [layoutManager glyphRangeForBoundingRect:targetRect
221                                             inTextContainer:textContainer];
222    numGlyphsUsed = NSMaxRange(range);
223    if (numGlyphsUsed < [layoutManager numberOfGlyphs]) {
224      // Didn't all fit, add a break, and grow the rect to try again.
225      NSRange charRange = [layoutManager glyphRangeForCharacterRange:range
226                                                actualCharacterRange:nil];
227      [workerStr insertString:kForcedWrapString atIndex:NSMaxRange(charRange)];
228      targetRect.size.height += lineHeight;
229    }
230  }
231#endif  // GTM_USE_TYPESETTER
232
233  // Return the string with forced wraps
234  return [[workerStr copy] autorelease];
235}
236
237+ (NSSize)sizeToFitView:(NSView *)view {
238  return SizeToFit(view, NSZeroPoint);
239}
240
241+ (CGFloat)sizeToFitFixedWidthTextField:(NSTextField *)textField {
242  NSRect initialFrame = [textField frame];
243  NSRect sizeRect = NSMakeRect(0, 0, NSWidth(initialFrame), CGFLOAT_MAX);
244  NSSize newSize = [[textField cell] cellSizeForBounds:sizeRect];
245  newSize.width = NSWidth(initialFrame);
246  [textField setFrameSize:newSize];
247  return newSize.height - NSHeight(initialFrame);
248}
249
250#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5
251
252+ (CGFloat)sizeToFitFixedHeightTextField:(NSTextField *)textField {
253  return [self sizeToFitFixedHeightTextField:textField minWidth:(CGFloat)0];
254}
255
256+ (CGFloat)sizeToFitFixedHeightTextField:(NSTextField *)textField
257                                minWidth:(NSUInteger)minWidth {
258  NSRect initialRect = [textField frame];
259  NSCell *cell = [textField cell];
260  NSSize titleSize = [cell titleRectForBounds:initialRect].size;
261
262  // Find linebreak point, and keep trying them until we're under the height
263  // requested.
264  NSString *str = [textField stringValue];
265  CFStringTokenizerRef tokenizer =
266      CFStringTokenizerCreate(NULL,
267                              (CFStringRef)str,
268                              CFRangeMake(0, [str length]),
269                              kCFStringTokenizerUnitLineBreak,
270                              NULL);
271  if (!tokenizer) {
272    _GTMDevAssert(tokenizer, @"failed to get a tokenizer");
273    return 0.0;
274  }
275  NSCell *workerCell = [[cell copy] autorelease];
276
277  // Loop trying line break points until the height fits.
278  while (1) {
279    CFStringTokenizerTokenType tokenType =
280        CFStringTokenizerAdvanceToNextToken(tokenizer);
281    if (tokenType == kCFStringTokenizerTokenNone) {
282      // Reached the end without ever find a good width, how?
283      _GTMDevAssert(0, @"Failed to find a good size?!");
284      [textField sizeToFit];
285      break;
286    }
287    CFRange tokenRange = CFStringTokenizerGetCurrentTokenRange(tokenizer);
288
289    NSRange subStringRange =
290      NSMakeRange(0, tokenRange.location + tokenRange.length);
291    NSString *subString = [str substringWithRange:subStringRange];
292
293    // Find how wide the cell would be for this sub string.
294    [workerCell setStringValue:subString];
295    CGFloat testWidth = [workerCell cellSize].width;
296
297    // Find the overall size if wrapped to this width.
298    NSRect sizeRect = NSMakeRect(0, 0, testWidth, CGFLOAT_MAX);
299    NSSize newSize = [cell cellSizeForBounds:sizeRect];
300    if (newSize.height <= titleSize.height) {
301      [textField setFrameSize:newSize];
302      break;
303    }
304  }
305
306  CFRelease(tokenizer);
307
308  NSSize fixedSize = [textField frame].size;
309  NSSize finalSize = NSMakeSize(fixedSize.width, NSHeight(initialRect));
310
311  // Enforce the minWidth
312  if (minWidth > fixedSize.width) {
313    finalSize.width = minWidth;
314  }
315  // Make integral.
316  finalSize.width = ceil(fixedSize.width);
317  finalSize.height = ceil(fixedSize.height);
318  if (!NSEqualSizes(fixedSize, finalSize)) {
319    [textField setFrameSize:finalSize];
320  }
321
322  // Return how much things changed
323  return finalSize.width - NSWidth(initialRect);
324}
325
326#endif  // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5
327
328+ (void)wrapButtonTitleForWidth:(NSButton *)button {
329  NSCell *cell = [button cell];
330  NSRect frame = [button frame];
331
332  NSRect titleFrame = [cell titleRectForBounds:frame];
333
334  NSString* newTitle = [self wrapString:[button title]
335                                  width:NSWidth(titleFrame)
336                                   font:[button font]];
337  [button setTitle:newTitle];
338}
339
340+ (void)wrapRadioGroupForWidth:(NSMatrix *)radioGroup {
341  NSSize cellSize = [radioGroup cellSize];
342  NSRect tmpRect = NSMakeRect(0, 0, cellSize.width, cellSize.height);
343  NSFont *font = [radioGroup font];
344
345  NSCell *cell;
346  GTM_FOREACH_OBJECT(cell, [radioGroup cells]) {
347    NSRect titleFrame = [cell titleRectForBounds:tmpRect];
348    NSString* newTitle = [self wrapString:[cell title]
349                                    width:NSWidth(titleFrame)
350                                     font:font];
351    [cell setTitle:newTitle];
352  }
353}
354
355+ (void)resizeWindowWithoutAutoResizingSubViews:(NSWindow*)window
356                                          delta:(NSSize)delta {
357  NSView *contentView = [window contentView];
358
359  // Clear autosizesSubviews (saving the state).
360  BOOL autoresizesSubviews = [contentView autoresizesSubviews];
361  if (autoresizesSubviews) {
362    [contentView setAutoresizesSubviews:NO];
363  }
364
365  NSRect rect = [contentView convertRect:[window frame] fromView:nil];
366  rect.size.width += delta.width;
367  rect.size.height += delta.height;
368  rect = [contentView convertRect:rect toView:nil];
369  [window setFrame:rect display:NO];
370  // For some reason the content view is resizing, but some times not adjusting
371  // its origin, so correct it manually.
372  [contentView setFrameOrigin:NSMakePoint(0, 0)];
373
374  // Restore autosizesSubviews.
375  if (autoresizesSubviews) {
376    [contentView setAutoresizesSubviews:YES];
377  }
378}
379
380+ (void)resizeViewWithoutAutoResizingSubViews:(NSView*)view
381                                        delta:(NSSize)delta {
382  // Clear autosizesSubviews (saving the state).
383  BOOL autoresizesSubviews = [view autoresizesSubviews];
384  if (autoresizesSubviews) {
385    [view setAutoresizesSubviews:NO];
386  }
387
388  NSRect rect = [view frame];
389  rect.size.width += delta.width;
390  rect.size.height += delta.height;
391  [view setFrame:rect];
392
393  // Restore autosizesSubviews.
394  if (autoresizesSubviews) {
395    [view setAutoresizesSubviews:YES];
396  }
397}
398
399@end
400
401@implementation GTMWidthBasedTweaker
402
403- (CGFloat)changedWidth {
404  return widthChange_;
405}
406
407- (CGFloat)tweakLayoutWithOffset:(NSPoint)offset {
408  NSArray *subviews = [self subviews];
409  if (![subviews count]) {
410    widthChange_ = 0.0;
411    return widthChange_;
412  }
413
414  BOOL sumMode = NO;
415  NSMutableArray *rightAlignedSubViews = nil;
416  NSMutableArray *rightAlignedSubViewDeltas = nil;
417  if ([subviews count] > 1) {
418    // Check if the frames are in a row by seeing if when they are left aligned
419    // they overlap.  If they don't overlap in this case, it means they are
420    // probably stacked instead.
421    NSRect rect1 = [[subviews objectAtIndex:0] frame];
422    NSRect rect2 = [[subviews objectAtIndex:1] frame];
423    rect1.origin.x = rect2.origin.x = 0;
424    if (NSIntersectsRect(rect1, rect2)) {
425      // No, so walk them x order moving them along so they don't overlap.
426      sumMode = YES;
427      subviews = [subviews sortedArrayUsingFunction:CompareFrameX context:NULL];
428    } else {
429      // Since they are vertical, any views pinned to the right will have to be
430      // shifted after we finish figuring out the final size.
431      rightAlignedSubViews = [NSMutableArray array];
432      rightAlignedSubViewDeltas = [NSMutableArray array];
433    }
434  }
435
436  // Size our subviews
437  NSView *subView;
438  CGFloat finalDelta = sumMode ? 0 : -CGFLOAT_MAX;
439  NSPoint subViewOffset = NSZeroPoint;
440  GTM_FOREACH_OBJECT(subView, subviews) {
441    if (sumMode) {
442      subViewOffset.x = finalDelta;
443    }
444    CGFloat delta = SizeToFit(subView, subViewOffset).width;
445    if (sumMode) {
446      finalDelta += delta;
447    } else {
448      if (delta > finalDelta) {
449        finalDelta = delta;
450      }
451    }
452    // Track the right anchored subviews size changes so we can update them
453    // once we know this view's size.
454    if (IsRightAnchored(subView)) {
455      [rightAlignedSubViews addObject:subView];
456#if CGFLOAT_IS_DOUBLE
457      NSNumber *nsDelta = [NSNumber numberWithDouble:delta];
458#else
459      NSNumber *nsDelta = [NSNumber numberWithFloat:delta];
460#endif
461      [rightAlignedSubViewDeltas addObject:nsDelta];
462    }
463  }
464
465  // Are we pinned to the right of our parent?
466  BOOL rightAnchored = IsRightAnchored(self);
467
468  // Adjust our size (turn off auto resize, because we just fixed up all the
469  // objects within us).
470  BOOL autoresizesSubviews = [self autoresizesSubviews];
471  if (autoresizesSubviews) {
472    [self setAutoresizesSubviews:NO];
473  }
474  NSRect selfFrame = [self frame];
475  selfFrame.size.width += finalDelta;
476  if (rightAnchored) {
477    // Right side is anchored, so we need to slide back to the left.
478    selfFrame.origin.x -= finalDelta;
479  }
480  selfFrame.origin.x += offset.x;
481  selfFrame.origin.y += offset.y;
482  [self setFrame:selfFrame];
483  if (autoresizesSubviews) {
484    [self setAutoresizesSubviews:autoresizesSubviews];
485  }
486
487  // Now spin over the list of right aligned view and their size changes
488  // fixing up their positions so they are still right aligned in our final
489  // view.
490  for (NSUInteger lp = 0; lp < [rightAlignedSubViews count]; ++lp) {
491    subView = [rightAlignedSubViews objectAtIndex:lp];
492    CGFloat delta = [[rightAlignedSubViewDeltas objectAtIndex:lp] doubleValue];
493    NSRect viewFrame = [subView frame];
494    viewFrame.origin.x += -delta + finalDelta;
495    [subView setFrame:viewFrame];
496  }
497
498  if (viewToSlideAndResize_) {
499    NSRect viewFrame = [viewToSlideAndResize_ frame];
500    if (!rightAnchored) {
501      // If our right wasn't anchored, this view slides (we push it right).
502      // (If our right was anchored, the assumption is the view is in front of
503      // us so its x shouldn't move.)
504      viewFrame.origin.x += finalDelta;
505    }
506    viewFrame.size.width -= finalDelta;
507    [viewToSlideAndResize_ setFrame:viewFrame];
508  }
509  if (viewToSlide_) {
510    NSRect viewFrame = [viewToSlide_ frame];
511    // Move the view the same direction we moved.
512    if (rightAnchored) {
513      viewFrame.origin.x -= finalDelta;
514    } else {
515      viewFrame.origin.x += finalDelta;
516    }
517    [viewToSlide_ setFrame:viewFrame];
518  }
519  if (viewToResize_) {
520    if ([viewToResize_ isKindOfClass:[NSWindow class]]) {
521      NSWindow *window = (NSWindow *)viewToResize_;
522      NSView *contentView = [window contentView];
523      NSRect windowFrame = [contentView convertRect:[window frame]
524                                           fromView:nil];
525      windowFrame.size.width += finalDelta;
526      windowFrame = [contentView convertRect:windowFrame toView:nil];
527      [window setFrame:windowFrame display:YES];
528      // For some reason the content view is resizing, but not adjusting its
529      // origin, so correct it manually.
530      [contentView setFrameOrigin:NSMakePoint(0, 0)];
531      // TODO: should we update min size?
532    } else {
533      NSRect viewFrame = [viewToResize_ frame];
534      viewFrame.size.width += finalDelta;
535      [viewToResize_ setFrame:viewFrame];
536      // TODO: should we check if this view is right anchored, and adjust its
537      // x position also?
538    }
539  }
540
541  widthChange_ = finalDelta;
542  return widthChange_;
543}
544
545@end
546
547#pragma mark -
548
549static NSSize SizeToFit(NSView *view, NSPoint offset) {
550
551  // If we've got one of us within us, recurse (for grids)
552  if ([view isKindOfClass:[GTMWidthBasedTweaker class]]) {
553    GTMWidthBasedTweaker *widthAlignmentBox = (GTMWidthBasedTweaker *)view;
554    return NSMakeSize([widthAlignmentBox tweakLayoutWithOffset:offset], 0);
555  }
556
557  NSRect oldFrame = [view frame];
558  NSRect fitFrame = oldFrame;
559  NSRect newFrame = oldFrame;
560
561  if ([view isKindOfClass:[NSTextField class]] &&
562      [(NSTextField *)view isEditable]) {
563    // Don't try to sizeToFit because edit fields really don't want to be sized
564    // to what is in them as they are for users to enter things so honor their
565    // current size.
566#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5
567  } else if ([view isKindOfClass:[NSPathControl class]]) {
568    // Don't try to sizeToFit because NSPathControls usually need to be able
569    // to display any path, so they shouldn't tight down to whatever they
570    // happen to be listing at the moment.
571#endif  // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5
572  } else {
573    // Genericaly fire a sizeToFit if it has one.
574    if ([view respondsToSelector:@selector(sizeToFit)]) {
575      [view performSelector:@selector(sizeToFit)];
576      fitFrame = [view frame];
577      newFrame = fitFrame;
578
579      if ([view isKindOfClass:[NSMatrix class]]) {
580        NSMatrix *matrix = (NSMatrix *)view;
581        // See note on kWrapperStringSlop for why this is done.
582        NSCell *cell;
583        GTM_FOREACH_OBJECT(cell, [matrix cells]) {
584          if ([[cell title] rangeOfString:kForcedWrapString].location !=
585              NSNotFound) {
586            newFrame.size.width += kWrapperStringSlop;
587            break;
588          }
589        }
590      }
591
592    }
593
594    if ([view isKindOfClass:[NSButton class]]) {
595      NSButton *button = (NSButton *)view;
596      // -[NSButton sizeToFit] gives much worse results than IB's Size to Fit
597      // option for standard push buttons.
598      if (([button bezelStyle] == NSRoundedBezelStyle) &&
599          ([[button cell] controlSize] == NSRegularControlSize)) {
600        // This is the amount of padding IB adds over a sizeToFit, empirically
601        // determined.
602        const CGFloat kExtraPaddingAmount = 12.0;
603        // Width is tricky, new buttons in IB are 96 wide, Carbon seems to have
604        // defaulted to 70, Cocoa seems to like 82.  But we go with 96 since
605        // that's what IB is doing these days.
606        const CGFloat kMinButtonWidth = (CGFloat)96.0;
607        newFrame.size.width = NSWidth(newFrame) + kExtraPaddingAmount;
608        if (NSWidth(newFrame) < kMinButtonWidth) {
609          newFrame.size.width = kMinButtonWidth;
610        }
611      } else if ([button bezelStyle] == NSTexturedRoundedBezelStyle &&
612                 [[button cell] controlSize] == NSRegularControlSize) {
613        // The round textured style needs to have a little extra padding,
614        // otherwise the baseline of the text sinks by a few pixels.
615        const CGFloat kExtraPaddingAmount = 4.0;
616        newFrame.size.width += kExtraPaddingAmount;
617      } else {
618        // See note on kWrapperStringSlop for why this is done.
619        NSString *title = [button title];
620        if ([title rangeOfString:kForcedWrapString].location != NSNotFound) {
621          newFrame.size.width += kWrapperStringSlop;
622        }
623      }
624    }
625  }
626
627  // Apply the offset, and see if we need to change the frame (again).
628  newFrame.origin.x += offset.x;
629  newFrame.origin.y += offset.y;
630  if (!NSEqualRects(fitFrame, newFrame)) {
631    [view setFrame:newFrame];
632  }
633
634  // Return how much we changed size.
635  return NSMakeSize(NSWidth(newFrame) - NSWidth(oldFrame),
636                    NSHeight(newFrame) - NSHeight(oldFrame));
637}
638
639static NSInteger CompareFrameX(id view1, id view2, void *context) {
640  CGFloat x1 = [view1 frame].origin.x;
641  CGFloat x2 = [view2 frame].origin.x;
642  if (x1 < x2)
643    return NSOrderedAscending;
644  else if (x1 > x2)
645    return NSOrderedDescending;
646  else
647    return NSOrderedSame;
648}
649
650static BOOL IsRightAnchored(NSView *view) {
651  NSUInteger autoresizing = [view autoresizingMask];
652  BOOL viewRightAnchored =
653   ((autoresizing & (NSViewMinXMargin | NSViewMaxXMargin)) == NSViewMinXMargin);
654  return viewRightAnchored;
655}