| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ios/chrome/browser/omnibox/ui/omnibox_container_view.h" |
| |
| #import "components/strings/grit/components_strings.h" |
| #import "ios/chrome/browser/omnibox/model/omnibox_metrics_recorder.h" |
| #import "ios/chrome/browser/omnibox/public/omnibox_constants.h" |
| #import "ios/chrome/browser/omnibox/public/omnibox_ui_features.h" |
| #import "ios/chrome/browser/omnibox/ui/omnibox_text_field_ios.h" |
| #import "ios/chrome/browser/omnibox/ui/omnibox_text_input.h" |
| #import "ios/chrome/browser/omnibox/ui/omnibox_text_view_ios.h" |
| #import "ios/chrome/browser/omnibox/ui/omnibox_thumbnail_button.h" |
| #import "ios/chrome/browser/shared/public/features/features.h" |
| #import "ios/chrome/browser/shared/ui/symbols/symbols.h" |
| #import "ios/chrome/browser/shared/ui/util/animation_util.h" |
| #import "ios/chrome/browser/shared/ui/util/layout_guide_names.h" |
| #import "ios/chrome/browser/shared/ui/util/rtl_geometry.h" |
| #import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h" |
| #import "ios/chrome/browser/shared/ui/util/util_swift.h" |
| #import "ios/chrome/common/material_timing.h" |
| #import "ios/chrome/common/ui/colors/semantic_color_names.h" |
| #import "ios/chrome/common/ui/util/constraints_ui_util.h" |
| #import "ios/chrome/common/ui/util/image_util.h" |
| #import "ios/chrome/common/ui/util/pointer_interaction_util.h" |
| #import "ios/chrome/grit/ios_strings.h" |
| #import "ios/chrome/grit/ios_theme_resources.h" |
| #import "skia/ext/skia_utils_ios.h" |
| #import "ui/base/l10n/l10n_util.h" |
| #import "ui/gfx/color_palette.h" |
| #import "ui/gfx/image/image.h" |
| |
| namespace { |
| |
| /// Width of the thumbnail. |
| const CGFloat kThumbnailWidth = 48; |
| /// Height of the thumbnail. |
| const CGFloat kThumbnailHeight = 40; |
| /// Space between the thumbnail image and the omnibox text. |
| const CGFloat kThumbnailImageTrailingMargin = 10; |
| /// Space between the leading icon and the thumbnail image. |
| const CGFloat kThumbnailImageLeadingMargin = 9; |
| |
| // The leading image margins when presented in the lens overlay. |
| const CGFloat kLeadingImageLeadingMarginLensOverlay = 12; |
| const CGFloat kLeadingImageTrailingMarginLensOverlay = 9; |
| |
| /// Size of the leading image when presented in composebox. |
| const CGFloat kLeadingImageSizeAIM = 22; |
| // The leading image margins when presented in composebox. |
| const CGFloat kLeadingImageLeadingMarginAIM = 7; |
| const CGFloat kLeadingImageTrailingMarginAIM = 11; |
| |
| /// Space between the clear button and the edge of the omnibox. |
| const CGFloat kTextInputViewClearButtonTrailingOffset = 4; |
| /// The maximum number of lines for the text view before it starts scrolling. |
| const int kMaxLines = 3; |
| |
| /// Clear button inset on all sides. |
| const CGFloat kClearButtonInset = 4.0f; |
| /// Clear button image size. |
| const CGFloat kClearButtonImageSize = 17.0f; |
| const CGFloat kClearButtonSize = 28.0f; |
| |
| /// Whether the omnibox is using the text view instead of the text field. |
| bool UseTextView(OmniboxPresentationContext presentation_context) { |
| if (presentation_context == OmniboxPresentationContext::kLocationBar) { |
| return IsMultilineBrowserOmniboxEnabled(); |
| } else if (presentation_context == OmniboxPresentationContext::kComposebox) { |
| return YES; |
| } |
| return NO; |
| } |
| |
| /// Creates and configures the leading image view. |
| UIImageView* CreateLeadingImageView( |
| UIColor* icon_tint, |
| OmniboxPresentationContext presentation_context) { |
| UIImageView* leading_image_view = [[UIImageView alloc] init]; |
| leading_image_view.translatesAutoresizingMaskIntoConstraints = NO; |
| leading_image_view.tintColor = icon_tint; |
| if (presentation_context == OmniboxPresentationContext::kComposebox) { |
| leading_image_view.contentMode = UIViewContentModeScaleAspectFit; |
| [NSLayoutConstraint activateConstraints:@[ |
| [leading_image_view.widthAnchor |
| constraintEqualToConstant:kLeadingImageSizeAIM], |
| [leading_image_view.heightAnchor |
| constraintEqualToConstant:kLeadingImageSizeAIM], |
| ]]; |
| } else { |
| leading_image_view.contentMode = UIViewContentModeCenter; |
| [NSLayoutConstraint activateConstraints:@[ |
| [leading_image_view.widthAnchor |
| constraintEqualToConstant:kOmniboxLeadingImageSize], |
| [leading_image_view.heightAnchor |
| constraintEqualToConstant:kOmniboxLeadingImageSize], |
| ]]; |
| } |
| |
| return leading_image_view; |
| } |
| |
| /// Creates and configures the thumbnail button. |
| OmniboxThumbnailButton* CreateThumbnailButton() { |
| OmniboxThumbnailButton* thumbnail_button = |
| [[OmniboxThumbnailButton alloc] init]; |
| thumbnail_button.translatesAutoresizingMaskIntoConstraints = NO; |
| thumbnail_button.accessibilityLabel = |
| l10n_util::GetNSString(IDS_IOS_OMNIBOX_REMOVE_THUMBNAIL_LABEL); |
| thumbnail_button.accessibilityHint = |
| l10n_util::GetNSString(IDS_IOS_OMNIBOX_REMOVE_THUMBNAIL_HINT); |
| thumbnail_button.hidden = YES; |
| [NSLayoutConstraint activateConstraints:@[ |
| [thumbnail_button.widthAnchor constraintEqualToConstant:kThumbnailWidth], |
| [thumbnail_button.heightAnchor constraintEqualToConstant:kThumbnailHeight], |
| ]]; |
| return thumbnail_button; |
| } |
| |
| /// Creates and configures the clear button. |
| UIButton* CreateClearButton() { |
| UIButtonConfiguration* conf = |
| [UIButtonConfiguration plainButtonConfiguration]; |
| conf.image = |
| DefaultSymbolWithPointSize(kXMarkCircleFillSymbol, kClearButtonImageSize); |
| conf.contentInsets = |
| NSDirectionalEdgeInsetsMake(kClearButtonInset, kClearButtonInset, |
| kClearButtonInset, kClearButtonInset); |
| UIButton* clear_button = [UIButton buttonWithType:UIButtonTypeSystem]; |
| clear_button.translatesAutoresizingMaskIntoConstraints = NO; |
| clear_button.configuration = conf; |
| clear_button.tintColor = [UIColor colorNamed:kTextfieldPlaceholderColor]; |
| SetA11yLabelAndUiAutomationName(clear_button, IDS_IOS_ACCNAME_CLEAR_TEXT, |
| @"Clear Text"); |
| clear_button.pointerInteractionEnabled = YES; |
| clear_button.pointerStyleProvider = |
| CreateLiftEffectCirclePointerStyleProvider(); |
| [NSLayoutConstraint activateConstraints:@[ |
| [clear_button.widthAnchor constraintEqualToConstant:kClearButtonSize], |
| [clear_button.heightAnchor constraintEqualToConstant:kClearButtonSize], |
| ]]; |
| return clear_button; |
| } |
| |
| } // namespace |
| |
| #pragma mark - OmniboxContainerView |
| |
| @interface OmniboxContainerView () <OmniboxTextViewHeightDelegate> |
| |
| // Redefined as readwrite. |
| @property(nonatomic, strong) UIButton* clearButton; |
| |
| @end |
| |
| @implementation OmniboxContainerView { |
| /// The leading image view. Used for autocomplete icons. |
| UIImageView* _leadingImageView; |
| // The image thumnail button. |
| OmniboxThumbnailButton* _thumbnailButton; |
| // The text input view. |
| UIView<OmniboxTextInput>* _textInputView; |
| // Stores the text view for height adjustment. |
| OmniboxTextViewIOS* _textView; |
| |
| /// The context in which the omnibox is presented. |
| OmniboxPresentationContext _presentationContext; |
| |
| // The constraint for the textfield's leading anchor when the thumbnail is |
| // visible. |
| NSLayoutConstraint* _textInputViewLeadingToThumbnailConstraint; |
| // The constraint for the textfield's leading anchor when the thumbnail is |
| // hidden. |
| NSLayoutConstraint* _textInputViewLeadingToIconConstraint; |
| // Text input height constraint. |
| NSLayoutConstraint* _textInputHeightConstraint; |
| // The last known width of the text view, used to avoid redundant height |
| // calculations. |
| CGFloat _lastKnownTextViewWidth; |
| } |
| |
| @synthesize heightDelegate = _heightDelegate; |
| |
| #pragma mark - Public |
| |
| - (instancetype)initWithFrame:(CGRect)frame |
| textColor:(UIColor*)textColor |
| textInputTint:(UIColor*)textInputTint |
| iconTint:(UIColor*)iconTint |
| presentationContext:(OmniboxPresentationContext)presentationContext { |
| self = [super initWithFrame:frame]; |
| if (self) { |
| _presentationContext = presentationContext; |
| _leadingImageView = CreateLeadingImageView(iconTint, presentationContext); |
| self.clearButton = CreateClearButton(); |
| [self createAndAddTextInputViewWithTextColor:textColor |
| textInputTint:textInputTint]; |
| |
| [self addSubview:_leadingImageView]; |
| [self addSubview:self.clearButton]; |
| |
| // Constraints. |
| [_textInputView |
| setContentCompressionResistancePriority:UILayoutPriorityDefaultLow |
| forAxis: |
| UILayoutConstraintAxisHorizontal]; |
| [_textInputView setContentHuggingPriority:UILayoutPriorityDefaultLow |
| forAxis:UILayoutConstraintAxisHorizontal]; |
| |
| NSLayoutAnchor* referenceCenterYAnchor = |
| _textInputView.viewForVerticalAlignment.centerYAnchor; |
| |
| CGFloat leadingImageLeadingOffset = kOmniboxLeadingImageViewEdgeOffset; |
| if (_presentationContext == OmniboxPresentationContext::kLensOverlay) { |
| leadingImageLeadingOffset = kLeadingImageLeadingMarginLensOverlay; |
| } else if (_presentationContext == |
| OmniboxPresentationContext::kComposebox) { |
| leadingImageLeadingOffset = kLeadingImageLeadingMarginAIM; |
| } |
| |
| [NSLayoutConstraint activateConstraints:@[ |
| [_leadingImageView.leadingAnchor |
| constraintEqualToAnchor:self.leadingAnchor |
| constant:leadingImageLeadingOffset], |
| [_leadingImageView.centerYAnchor |
| constraintEqualToAnchor:referenceCenterYAnchor], |
| [_textInputView.topAnchor constraintEqualToAnchor:self.topAnchor], |
| [_textInputView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor], |
| [self.clearButton.centerYAnchor |
| constraintEqualToAnchor:referenceCenterYAnchor], |
| [self.clearButton.trailingAnchor |
| constraintEqualToAnchor:self.trailingAnchor |
| constant:-kTextInputViewClearButtonTrailingOffset], |
| [_textInputView.trailingAnchor |
| constraintEqualToAnchor:self.clearButton.leadingAnchor], |
| ]]; |
| |
| // Thumbnail image view. |
| if (base::FeatureList::IsEnabled(kEnableLensOverlay)) { |
| _thumbnailButton = CreateThumbnailButton(); |
| [self addSubview:_thumbnailButton]; |
| [NSLayoutConstraint activateConstraints:@[ |
| [_thumbnailButton.leadingAnchor |
| constraintEqualToAnchor:_leadingImageView.trailingAnchor |
| constant:kThumbnailImageLeadingMargin], |
| [_thumbnailButton.centerYAnchor |
| constraintEqualToAnchor:referenceCenterYAnchor] |
| ]]; |
| |
| // The textInputView can be anchored to the thumbnail (if visible) or the |
| // leading icon (if thumbnail is hidden). |
| _textInputViewLeadingToThumbnailConstraint = [_textInputView.leadingAnchor |
| constraintEqualToAnchor:_thumbnailButton.trailingAnchor |
| constant:kThumbnailImageTrailingMargin]; |
| } |
| |
| CGFloat textInputViewLeadingOffset = kOmniboxTextFieldLeadingOffsetImage; |
| if (_presentationContext == OmniboxPresentationContext::kLensOverlay) { |
| textInputViewLeadingOffset = kLeadingImageTrailingMarginLensOverlay; |
| } else if (_presentationContext == |
| OmniboxPresentationContext::kComposebox) { |
| textInputViewLeadingOffset = kLeadingImageTrailingMarginAIM; |
| } |
| |
| _textInputViewLeadingToIconConstraint = [_textInputView.leadingAnchor |
| constraintEqualToAnchor:_leadingImageView.trailingAnchor |
| constant:textInputViewLeadingOffset]; |
| // By default, the thumbnail is hidden, so the text field is anchored to the |
| // leading icon. |
| _textInputViewLeadingToIconConstraint.active = YES; |
| } |
| return self; |
| } |
| |
| - (void)layoutSubviews { |
| [super layoutSubviews]; |
| if (_textView.bounds.size.width != _lastKnownTextViewWidth) { |
| _lastKnownTextViewWidth = _textView.bounds.size.width; |
| [self updateTextViewHeight]; |
| } |
| } |
| |
| - (void)setLeadingImage:(UIImage*)image |
| withAccessibilityIdentifier:(NSString*)accessibilityIdentifier { |
| _leadingImageView.image = image; |
| _leadingImageView.accessibilityIdentifier = accessibilityIdentifier; |
| } |
| |
| - (void)setLeadingImageScale:(CGFloat)scaleValue { |
| _leadingImageView.transform = |
| CGAffineTransformMakeScale(scaleValue, scaleValue); |
| } |
| |
| - (UIButton*)thumbnailButton { |
| return _thumbnailButton; |
| } |
| |
| - (UIImage*)thumbnailImage { |
| return _thumbnailButton.thumbnailImage; |
| } |
| |
| - (void)setThumbnailImage:(UIImage*)image { |
| [_thumbnailButton setThumbnailImage:image]; |
| [_thumbnailButton setHidden:!image]; |
| |
| BOOL thumbnailVisible = ![_thumbnailButton isHidden]; |
| if (_textInputViewLeadingToThumbnailConstraint) { |
| _textInputViewLeadingToThumbnailConstraint.active = thumbnailVisible; |
| } |
| _textInputViewLeadingToIconConstraint.active = !thumbnailVisible; |
| } |
| |
| - (void)setLayoutGuideCenter:(LayoutGuideCenter*)layoutGuideCenter { |
| _layoutGuideCenter = layoutGuideCenter; |
| [_layoutGuideCenter referenceView:_leadingImageView |
| underName:kOmniboxLeadingImageGuide]; |
| [_layoutGuideCenter referenceView:_textInputView |
| underName:kOmniboxTextFieldGuide]; |
| } |
| |
| - (void)setClearButtonHidden:(BOOL)isHidden { |
| self.clearButton.hidden = isHidden; |
| } |
| |
| - (id<OmniboxTextInput>)textInput { |
| return _textInputView; |
| } |
| |
| - (void)updateTextViewHeight { |
| if (!_textView) { |
| return; |
| } |
| |
| // Recalculate textView height and update it to clip and scroll if necessary. |
| CGFloat verticalPadding = |
| _textView.textContainerInset.top + _textView.textContainerInset.bottom; |
| CGFloat singleLineHeight = [self singleLineHeight]; |
| CGFloat maxHeight = (singleLineHeight * kMaxLines) + verticalPadding; |
| |
| // Calculate the height of the user text. |
| NSAttributedString* userText = _textView.attributedUserText; |
| if (_textView.isPreEditing) { |
| userText = [[NSAttributedString alloc] initWithString:@""]; |
| } |
| |
| // Calculate the precise drawing width. |
| UIEdgeInsets textContainerInsets = _textView.textContainerInset; |
| CGFloat lineFragmentPadding = _textView.textContainer.lineFragmentPadding; |
| CGFloat drawingWidth = _textView.bounds.size.width - |
| textContainerInsets.left - textContainerInsets.right - |
| lineFragmentPadding * 2.0; |
| if (drawingWidth < 0) { |
| drawingWidth = 0; |
| } |
| CGFloat userTextHeight = 0; |
| if (userText.length > 0) { |
| userTextHeight = [self heightForAttributedText:userText |
| withDrawingWidth:drawingWidth]; |
| } |
| if (!userTextHeight) { |
| userTextHeight = singleLineHeight; |
| } |
| CGFloat newHeight = ceilf(userTextHeight + verticalPadding); |
| |
| NSInteger numberOfLines = round(userTextHeight / singleLineHeight); |
| [self.metricsRecorder setNumberOfLines:numberOfLines]; |
| _textView.textContainer.maximumNumberOfLines = numberOfLines; |
| NSLineBreakMode defaultLineBreakMode = |
| [self lineBreakModeForUserText:userText]; |
| [self updateLastLineClipping:defaultLineBreakMode]; |
| |
| newHeight = MIN(newHeight, maxHeight); |
| if (!_textInputHeightConstraint) { |
| _textInputHeightConstraint = |
| [_textView.heightAnchor constraintEqualToConstant:newHeight]; |
| _textInputHeightConstraint.active = YES; |
| } else { |
| _textInputHeightConstraint.constant = newHeight; |
| } |
| _textView.scrollEnabled = userTextHeight > maxHeight; |
| [self.heightDelegate textFieldViewContaining:self didChangeHeight:newHeight]; |
| } |
| |
| /// Updates the paragraph style to clip the last line. |
| - (void)updateLastLineClipping:(NSLineBreakMode)defaultLineBreakMode { |
| NSTextStorage* textStorage = _textView.textStorage; |
| NSRange fullRange = NSMakeRange(0, textStorage.length); |
| |
| [self applyLineBreakMode:defaultLineBreakMode |
| toMutableAttributedString:textStorage |
| inRange:fullRange]; |
| |
| NSLayoutManager* layoutManager = _textView.layoutManager; |
| NSUInteger numberOfGlyphs = [layoutManager numberOfGlyphs]; |
| NSUInteger maxLines = _textView.textContainer.maximumNumberOfLines; |
| |
| if (numberOfGlyphs == 0 || maxLines == 0) { |
| return; |
| } |
| |
| // Determine the actual number of lines. |
| NSUInteger lineCount = 0; |
| NSRange lineRange; |
| for (NSUInteger glyphIndex = 0; glyphIndex < numberOfGlyphs; lineCount++) { |
| [layoutManager lineFragmentRectForGlyphAtIndex:glyphIndex |
| effectiveRange:&lineRange]; |
| glyphIndex = NSMaxRange(lineRange); |
| } |
| |
| if (lineCount >= maxLines) { |
| // Find the glyph index at the start of the line to be clipped. |
| NSUInteger clipStartGlyphIndex = 0; |
| for (NSUInteger i = 0; i < maxLines - 1; i++) { |
| if (clipStartGlyphIndex >= numberOfGlyphs) { |
| break; |
| } |
| [layoutManager lineFragmentRectForGlyphAtIndex:clipStartGlyphIndex |
| effectiveRange:&lineRange]; |
| clipStartGlyphIndex = NSMaxRange(lineRange); |
| } |
| |
| // Convert the glyph index to a character index. |
| NSUInteger clipStartCharIndex = |
| [layoutManager characterIndexForGlyphAtIndex:clipStartGlyphIndex]; |
| |
| // Apply clipping to the last line and beyond. |
| if (clipStartCharIndex < textStorage.length) { |
| NSRange clipRange = NSMakeRange(clipStartCharIndex, |
| textStorage.length - clipStartCharIndex); |
| [self applyLineBreakMode:NSLineBreakByClipping |
| toMutableAttributedString:textStorage |
| inRange:clipRange]; |
| } |
| } |
| } |
| |
| #pragma mark - TextFieldViewContaining |
| |
| - (UIView*)textFieldView { |
| return _textInputView; |
| } |
| |
| #pragma mark - Private |
| |
| /// Creates the text input view and adds it to the view hierarchy. |
| - (void)createAndAddTextInputViewWithTextColor:(UIColor*)textColor |
| textInputTint:(UIColor*)textInputTint { |
| if (UseTextView(_presentationContext)) { |
| OmniboxTextViewIOS* textView = |
| [[OmniboxTextViewIOS alloc] initWithFrame:CGRectZero |
| textColor:textColor |
| tintColor:textInputTint |
| presentationContext:_presentationContext]; |
| textView.translatesAutoresizingMaskIntoConstraints = NO; |
| [self addSubview:textView]; |
| // The placeholder must be added as a sibling to the textview. Constraints |
| // are handled internally in the text view. |
| UILabel* placeholderLabel = [[UILabel alloc] init]; |
| placeholderLabel.translatesAutoresizingMaskIntoConstraints = NO; |
| [self addSubview:placeholderLabel]; |
| textView.placeholderLabel = placeholderLabel; |
| textView.heightDelegate = self; |
| _textView = textView; |
| _textInputView = textView; |
| [self updateTextViewHeight]; |
| } else { |
| OmniboxTextFieldIOS* textField = |
| [[OmniboxTextFieldIOS alloc] initWithFrame:CGRectZero |
| textColor:textColor |
| tintColor:textInputTint |
| presentationContext:_presentationContext]; |
| // Do not use the system clear button. Use a custom view instead. |
| textField.clearButtonMode = UITextFieldViewModeNever; |
| textField.translatesAutoresizingMaskIntoConstraints = NO; |
| [self addSubview:textField]; |
| _textInputView = textField; |
| } |
| } |
| |
| #pragma mark - OmniboxTextViewHeightDelegate |
| |
| - (void)textViewContentChanged:(OmniboxTextViewIOS*)textView { |
| [self updateTextViewHeight]; |
| } |
| |
| #pragma mark - Private |
| |
| /// Returns the height of a single line of text with the current font. |
| - (CGFloat)singleLineHeight { |
| UIFont* font = _textView.font ?: _textView.currentFont; |
| // Create a sample attributed string for one line. |
| NSAttributedString* singleLineSampler = |
| [[NSAttributedString alloc] initWithString:@"T" |
| attributes:@{NSFontAttributeName : font}]; |
| CGSize singleLineConstraint = CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX); |
| NSStringDrawingOptions options = |
| NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading; |
| CGRect singleLineBoundingRect = |
| [singleLineSampler boundingRectWithSize:singleLineConstraint |
| options:options |
| context:nil]; |
| CGFloat measuredSingleLineHeight = ceilf(singleLineBoundingRect.size.height); |
| |
| // If for some reason measurement fails, fall back to font.lineHeight. |
| if (measuredSingleLineHeight <= 0) { |
| measuredSingleLineHeight = font.lineHeight; |
| } |
| return measuredSingleLineHeight; |
| } |
| |
| /// Computes the height needed to layout `attributedText` with `drawingWidth`. |
| /// The height is computed with `lineBreakModeForUserText`. |
| - (CGFloat)heightForAttributedText:(NSAttributedString*)attributedText |
| withDrawingWidth:(CGFloat)drawingWidth { |
| if (attributedText.length == 0) { |
| return 0; |
| } |
| NSMutableAttributedString* mutableString = [[NSMutableAttributedString alloc] |
| initWithAttributedString:attributedText]; |
| NSLineBreakMode lineBreakMode = |
| [self lineBreakModeForUserText:attributedText]; |
| NSRange fullRange = NSMakeRange(0, mutableString.length); |
| [self applyLineBreakMode:lineBreakMode |
| toMutableAttributedString:mutableString |
| inRange:fullRange]; |
| CGSize constraintSize = CGSizeMake(drawingWidth, CGFLOAT_MAX); |
| NSStringDrawingOptions options = |
| NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading; |
| CGRect boundingRect = [mutableString boundingRectWithSize:constraintSize |
| options:options |
| context:nil]; |
| return ceilf(boundingRect.size.height); |
| } |
| |
| /// Returns the line break mode to apply for the text. |
| - (NSLineBreakMode)lineBreakModeForUserText:(NSAttributedString*)text { |
| BOOL containsWhitespace = |
| [text.string |
| rangeOfCharacterFromSet:[NSCharacterSet |
| whitespaceAndNewlineCharacterSet]] |
| .location != NSNotFound; |
| NSLineBreakMode defaultLineBreakMode = containsWhitespace |
| ? NSLineBreakByWordWrapping |
| : NSLineBreakByCharWrapping; |
| return defaultLineBreakMode; |
| } |
| |
| /// Applies a line break mode to an attributed string within a specific range, |
| /// preserving all other paragraph style properties. |
| /// |
| /// @param mutableAttributedString The mutable attributed string to modify. |
| /// @param lineBreakMode The new NSLineBreakMode to apply. |
| /// @param range The range within the attributed string to apply the line break |
| /// mode. |
| - (void)applyLineBreakMode:(NSLineBreakMode)lineBreakMode |
| toMutableAttributedString: |
| (NSMutableAttributedString*)mutableAttributedString |
| inRange:(NSRange)range { |
| [mutableAttributedString |
| enumerateAttribute:NSParagraphStyleAttributeName |
| inRange:range |
| options:0 |
| usingBlock:^(id value, NSRange currentRange, BOOL* stop) { |
| NSParagraphStyle* existingStyle = (NSParagraphStyle*)value; |
| |
| // If a paragraph style exists, copy it. Otherwise, create a new |
| // default one. |
| NSMutableParagraphStyle* newParagraphStyle = |
| existingStyle ? [existingStyle mutableCopy] |
| : [[NSMutableParagraphStyle alloc] init]; |
| |
| // Set the desired line break mode. |
| newParagraphStyle.lineBreakMode = lineBreakMode; |
| |
| // Re-apply the modified style to the original range. |
| [mutableAttributedString |
| addAttribute:NSParagraphStyleAttributeName |
| value:newParagraphStyle |
| range:currentRange]; |
| }]; |
| } |
| |
| @end |