1// Copyright 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#import "ios/chrome/browser/ui/util/uikit_ui_util.h" 6 7#import <Accelerate/Accelerate.h> 8#import <Foundation/Foundation.h> 9#import <QuartzCore/QuartzCore.h> 10#import <UIKit/UIKit.h> 11#include <stddef.h> 12#include <stdint.h> 13#include <cmath> 14 15#include "base/check_op.h" 16#include "base/ios/ios_util.h" 17#include "base/mac/foundation_util.h" 18#include "base/notreached.h" 19#include "base/numerics/math_constants.h" 20#include "ios/chrome/browser/system_flags.h" 21#include "ios/chrome/browser/ui/ui_feature_flags.h" 22#include "ios/chrome/browser/ui/util/dynamic_type_util.h" 23#include "ios/chrome/browser/ui/util/rtl_geometry.h" 24#include "ios/chrome/browser/ui/util/ui_util.h" 25#include "ios/web/public/thread/web_thread.h" 26#include "ui/base/l10n/l10n_util.h" 27#include "ui/base/l10n/l10n_util_mac.h" 28#include "ui/base/resource/resource_bundle.h" 29#include "ui/gfx/ios/uikit_util.h" 30#include "ui/gfx/scoped_cg_context_save_gstate_mac.h" 31 32#if !defined(__has_feature) || !__has_feature(objc_arc) 33#error "This file requires ARC support." 34#endif 35 36namespace { 37 38// Store a reference to the current first responder. 39UIResponder* g_first_responder = nil; 40 41} // namespace 42 43// Category used to get the first responder. 44@implementation UIResponder (FirstResponder) 45 46- (void)cr_markSelfCurrentFirstResponder { 47 g_first_responder = self; 48} 49 50@end 51 52void SetA11yLabelAndUiAutomationName( 53 NSObject<UIAccessibilityIdentification>* element, 54 int idsAccessibilityLabel, 55 NSString* englishUiAutomationName) { 56 [element setAccessibilityLabel:l10n_util::GetNSString(idsAccessibilityLabel)]; 57 [element setAccessibilityIdentifier:englishUiAutomationName]; 58} 59 60void SetUILabelScaledFont(UILabel* label, UIFont* font) { 61 label.font = [[UIFontMetrics defaultMetrics] scaledFontForFont:font]; 62 label.adjustsFontForContentSizeCategory = YES; 63} 64 65void MaybeSetUILabelScaledFont(BOOL maybe, UILabel* label, UIFont* font) { 66 if (maybe) { 67 SetUILabelScaledFont(label, font); 68 } else { 69 label.font = font; 70 } 71} 72 73void SetUITextFieldScaledFont(UITextField* textField, UIFont* font) { 74 textField.font = [[UIFontMetrics defaultMetrics] scaledFontForFont:font]; 75 textField.adjustsFontForContentSizeCategory = YES; 76} 77 78void MaybeSetUITextFieldScaledFont(BOOL maybe, 79 UITextField* textField, 80 UIFont* font) { 81 if (maybe) { 82 SetUITextFieldScaledFont(textField, font); 83 } else { 84 textField.font = font; 85 } 86} 87 88UIImage* CaptureViewWithOption(UIView* view, 89 CGFloat scale, 90 CaptureViewOption option) { 91 UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO /* not opaque */, 92 scale); 93 if (option != kClientSideRendering) { 94 [view drawViewHierarchyInRect:view.bounds 95 afterScreenUpdates:option == kAfterScreenUpdate]; 96 } else { 97 CGContext* context = UIGraphicsGetCurrentContext(); 98 [view.layer renderInContext:context]; 99 } 100 UIImage* image = UIGraphicsGetImageFromCurrentImageContext(); 101 UIGraphicsEndImageContext(); 102 return image; 103} 104 105UIImage* CaptureView(UIView* view, CGFloat scale) { 106 return CaptureViewWithOption(view, scale, kNoCaptureOption); 107} 108 109UIImage* GreyImage(UIImage* image) { 110 DCHECK(image); 111 // Grey images are always non-retina to improve memory performance. 112 UIGraphicsBeginImageContextWithOptions(image.size, YES, 1.0); 113 CGRect greyImageRect = CGRectMake(0, 0, image.size.width, image.size.height); 114 [image drawInRect:greyImageRect blendMode:kCGBlendModeLuminosity alpha:1.0]; 115 UIImage* greyImage = UIGraphicsGetImageFromCurrentImageContext(); 116 UIGraphicsEndImageContext(); 117 return greyImage; 118} 119 120UIImage* NativeReversableImage(int imageID, BOOL reversable) { 121 DCHECK(imageID); 122 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 123 UIImage* image = rb.GetNativeImageNamed(imageID).ToUIImage(); 124 return (reversable && UseRTLLayout()) 125 ? [image imageFlippedForRightToLeftLayoutDirection] 126 : image; 127} 128 129UIImage* NativeImage(int imageID) { 130 return NativeReversableImage(imageID, NO); 131} 132 133UIImage* ResizeImage(UIImage* image, 134 CGSize targetSize, 135 ProjectionMode projectionMode) { 136 return ResizeImage(image, targetSize, projectionMode, NO); 137} 138 139UIImage* ResizeImage(UIImage* image, 140 CGSize targetSize, 141 ProjectionMode projectionMode, 142 BOOL opaque) { 143 CGSize revisedTargetSize; 144 CGRect projectTo; 145 146 CalculateProjection([image size], targetSize, projectionMode, 147 revisedTargetSize, projectTo); 148 149 if (CGRectEqualToRect(projectTo, CGRectZero)) 150 return nil; 151 152 // Resize photo. Use UIImage drawing methods because they respect 153 // UIImageOrientation as opposed to CGContextDrawImage(). 154 UIGraphicsBeginImageContextWithOptions(revisedTargetSize, opaque, 155 image.scale); 156 [image drawInRect:projectTo]; 157 UIImage* resizedPhoto = UIGraphicsGetImageFromCurrentImageContext(); 158 UIGraphicsEndImageContext(); 159 return resizedPhoto; 160} 161 162UIImage* TintImage(UIImage* image, UIColor* color) { 163 DCHECK(image); 164 DCHECK(image.CGImage); 165 DCHECK_GE(image.size.width * image.size.height, 1); 166 DCHECK(color); 167 168 CGRect rect = {CGPointZero, image.size}; 169 170 UIGraphicsBeginImageContextWithOptions(rect.size /* bitmap size */, 171 NO /* opaque? */, 172 0.0 /* main screen scale */); 173 CGContextRef imageContext = UIGraphicsGetCurrentContext(); 174 CGContextSetShouldAntialias(imageContext, true); 175 CGContextSetInterpolationQuality(imageContext, kCGInterpolationHigh); 176 177 // CoreGraphics and UIKit uses different axis. UIKit's y points downards, 178 // while CoreGraphic's points upwards. To keep the image correctly oriented, 179 // apply a mirror around the X axis by inverting the Y coordinates. 180 CGContextScaleCTM(imageContext, 1, -1); 181 CGContextTranslateCTM(imageContext, 0, -rect.size.height); 182 183 CGContextDrawImage(imageContext, rect, image.CGImage); 184 CGContextSetBlendMode(imageContext, kCGBlendModeSourceIn); 185 CGContextSetFillColorWithColor(imageContext, color.CGColor); 186 CGContextFillRect(imageContext, rect); 187 188 UIImage* outputImage = UIGraphicsGetImageFromCurrentImageContext(); 189 UIGraphicsEndImageContext(); 190 191 // Port the cap insets to the new image. 192 if (!UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero)) { 193 outputImage = [outputImage resizableImageWithCapInsets:image.capInsets]; 194 } 195 196 // Port the flipping status to the new image. 197 if (image.flipsForRightToLeftLayoutDirection) { 198 outputImage = [outputImage imageFlippedForRightToLeftLayoutDirection]; 199 } 200 201 return outputImage; 202} 203 204UIInterfaceOrientation GetInterfaceOrientation(UIWindow* window) { 205#if !defined(__IPHONE_13_0) || __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_13_0 206 return [[UIApplication sharedApplication] statusBarOrientation]; 207#else 208 return window.windowScene.interfaceOrientation; 209#endif 210} 211 212CGFloat CurrentKeyboardHeight(NSValue* keyboardFrameValue) { 213 return [keyboardFrameValue CGRectValue].size.height; 214} 215 216UIImage* ImageWithColor(UIColor* color) { 217 CGRect rect = CGRectMake(0, 0, 1, 1); 218 UIGraphicsBeginImageContext(rect.size); 219 CGContextRef context = UIGraphicsGetCurrentContext(); 220 CGContextSetFillColorWithColor(context, [color CGColor]); 221 CGContextFillRect(context, rect); 222 UIImage* image = UIGraphicsGetImageFromCurrentImageContext(); 223 UIGraphicsEndImageContext(); 224 return image; 225} 226 227UIImage* CircularImageFromImage(UIImage* image, CGFloat width) { 228 CGRect frame = 229 CGRectMakeAlignedAndCenteredAt(width / 2.0, width / 2.0, width); 230 231 UIGraphicsBeginImageContextWithOptions(frame.size, NO, 0.0); 232 CGContextRef context = UIGraphicsGetCurrentContext(); 233 234 CGContextBeginPath(context); 235 CGContextAddEllipseInRect(context, frame); 236 CGContextClosePath(context); 237 CGContextClip(context); 238 239 CGFloat scaleX = frame.size.width / image.size.width; 240 CGFloat scaleY = frame.size.height / image.size.height; 241 CGFloat scale = std::max(scaleX, scaleY); 242 CGContextScaleCTM(context, scale, scale); 243 244 [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; 245 246 image = UIGraphicsGetImageFromCurrentImageContext(); 247 UIGraphicsEndImageContext(); 248 249 return image; 250} 251 252bool IsPortrait(UIWindow* window) { 253 UIInterfaceOrientation orient = GetInterfaceOrientation(window); 254 return UIInterfaceOrientationIsPortrait(orient) || 255 orient == UIInterfaceOrientationUnknown; 256} 257 258bool IsLandscape(UIWindow* window) { 259 return UIInterfaceOrientationIsLandscape(GetInterfaceOrientation(window)); 260} 261 262bool IsCompactWidth(id<UITraitEnvironment> environment) { 263 return IsCompactWidth(environment.traitCollection); 264} 265 266bool IsCompactWidth(UITraitCollection* traitCollection) { 267 return traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact; 268} 269 270bool IsCompactHeight(id<UITraitEnvironment> environment) { 271 return IsCompactHeight(environment.traitCollection); 272} 273 274bool IsCompactHeight(UITraitCollection* traitCollection) { 275 return traitCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact; 276} 277 278bool IsRegularXRegularSizeClass(id<UITraitEnvironment> environment) { 279 return IsRegularXRegularSizeClass(environment.traitCollection); 280} 281 282bool IsRegularXRegularSizeClass(UITraitCollection* traitCollection) { 283 return traitCollection.verticalSizeClass == UIUserInterfaceSizeClassRegular && 284 traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassRegular; 285} 286 287bool ShouldShowCompactToolbar(id<UITraitEnvironment> environment) { 288 return ShouldShowCompactToolbar(environment.traitCollection); 289} 290 291bool ShouldShowCompactToolbar(UITraitCollection* traitCollection) { 292 return !IsRegularXRegularSizeClass(traitCollection); 293} 294 295bool IsSplitToolbarMode(id<UITraitEnvironment> environment) { 296 return IsSplitToolbarMode(environment.traitCollection); 297} 298 299bool IsSplitToolbarMode(UITraitCollection* traitCollection) { 300 return IsCompactWidth(traitCollection) && !IsCompactHeight(traitCollection); 301} 302 303UIView* GetFirstResponderSubview(UIView* view) { 304 if ([view isFirstResponder]) 305 return view; 306 307 for (UIView* subview in [view subviews]) { 308 UIView* firstResponder = GetFirstResponderSubview(subview); 309 if (firstResponder) 310 return firstResponder; 311 } 312 313 return nil; 314} 315 316UIResponder* GetFirstResponder() { 317 DCHECK_CURRENTLY_ON(web::WebThread::UI); 318 if (base::FeatureList::IsEnabled(kFirstResponderSendAction)) { 319 DCHECK_CURRENTLY_ON(web::WebThread::UI); 320 DCHECK(!g_first_responder); 321 [[UIApplication sharedApplication] 322 sendAction:@selector(cr_markSelfCurrentFirstResponder) 323 to:nil 324 from:nil 325 forEvent:nil]; 326 UIResponder* firstResponder = g_first_responder; 327 g_first_responder = nil; 328 return firstResponder; 329 } 330 return GetFirstResponderSubview([UIApplication sharedApplication].keyWindow); 331} 332 333// Trigger a haptic vibration for the user selecting an action. This is a no-op 334// for devices that do not support it. 335void TriggerHapticFeedbackForImpact(UIImpactFeedbackStyle impactStyle) { 336 // Although Apple documentation claims that UIFeedbackGenerator and its 337 // concrete subclasses are available on iOS 10+, they are not really 338 // available on an app whose deployment target is iOS 10.0 (iOS 10.1+ are 339 // okay) and Chrome will fail at dynamic link time and instantly crash. 340 // NSClassFromString() checks if Objective-C run-time has the class before 341 // using it. 342 Class generatorClass = NSClassFromString(@"UIImpactFeedbackGenerator"); 343 if (generatorClass) { 344 UIImpactFeedbackGenerator* generator = 345 [[generatorClass alloc] initWithStyle:impactStyle]; 346 [generator impactOccurred]; 347 } 348} 349 350// Trigger a haptic vibration for the change in selection. This is a no-op for 351// devices that do not support it. 352void TriggerHapticFeedbackForSelectionChange() { 353 // Although Apple documentation claims that UIFeedbackGenerator and its 354 // concrete subclasses are available on iOS 10+, they are not really 355 // available on an app whose deployment target is iOS 10.0 (iOS 10.1+ are 356 // okay) and Chrome will fail at dynamic link time and instantly crash. 357 // NSClassFromString() checks if Objective-C run-time has the class before 358 // using it. 359 Class generatorClass = NSClassFromString(@"UISelectionFeedbackGenerator"); 360 if (generatorClass) { 361 UISelectionFeedbackGenerator* generator = [[generatorClass alloc] init]; 362 [generator selectionChanged]; 363 } 364} 365 366// Trigger a haptic vibration for a notification. This is a no-op for devices 367// that do not support it. 368void TriggerHapticFeedbackForNotification(UINotificationFeedbackType type) { 369 // Although Apple documentation claims that UIFeedbackGenerator and its 370 // concrete subclasses are available on iOS 10+, they are not really 371 // available on an app whose deployment target is iOS 10.0 (iOS 10.1+ are 372 // okay) and Chrome will fail at dynamic link time and instantly crash. 373 // NSClassFromString() checks if Objective-C run-time has the class before 374 // using it. 375 Class generatorClass = NSClassFromString(@"UINotificationFeedbackGenerator"); 376 if (generatorClass) { 377 UINotificationFeedbackGenerator* generator = [[generatorClass alloc] init]; 378 [generator notificationOccurred:type]; 379 } 380} 381 382NSString* TextForTabCount(long count) { 383 if (count <= 0) 384 return @""; 385 if (count > 99) 386 return @":)"; 387 return [NSString stringWithFormat:@"%ld", count]; 388} 389 390void RegisterEditMenuItem(UIMenuItem* item) { 391 UIMenuController* menu = [UIMenuController sharedMenuController]; 392 NSArray<UIMenuItem*>* items = [menu menuItems]; 393 394 for (UIMenuItem* existingItem in items) { 395 if ([existingItem action] == [item action]) { 396 return; 397 } 398 } 399 400 items = items ? [items arrayByAddingObject:item] : @[ item ]; 401 402 [menu setMenuItems:items]; 403} 404 405UIView* ViewHierarchyRootForView(UIView* view) { 406 if (view.window) 407 return view.window; 408 409 if (!view.superview) 410 return view; 411 412 return ViewHierarchyRootForView(view.superview); 413} 414