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