chromium/ios/chrome/browser/ui/sharing/qr_generator/qr_generator_view_controller.mm

// Copyright 2020 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/ui/sharing/qr_generator/qr_generator_view_controller.h"

#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/ui/sharing/qr_generator/qr_generator_util.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/confirmation_alert/confirmation_alert_action_handler.h"
#import "ios/chrome/common/ui/util/button_util.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/common/ui/util/image_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util_mac.h"

namespace {

// Height and width of the QR code image, in points.
const CGFloat kQRCodeImageSize = 200.0;
constexpr CGFloat kGeneratedImagePadding = 20;
constexpr CGFloat kButtonMaxWidth = 327;
constexpr CGFloat kContentMaxWidth = 500;
constexpr CGFloat kBottomMargin = 24;
constexpr CGFloat kSymbolSize = 22;

}  // namespace

@interface QRGeneratorViewController ()

// Container view that will wrap the views making up the content.
@property(nonatomic, strong) UIStackView* stackView;

// URL of the page to generate a QR code for.
@property(nonatomic, copy) NSURL* pageURL;

@property(nonatomic, copy) NSString* pageTitle;

@property(nonatomic, strong) NSArray* regularHeightToolbarItems;
@property(nonatomic, strong) NSArray* compactHeightToolbarItems;
@property(nonatomic, strong) UIToolbar* topToolbar;

@property(nonatomic, strong)
    NSLayoutConstraint* regularHeightScrollViewBottomVerticalConstraint;
@property(nonatomic, strong)
    NSLayoutConstraint* compactHeightScrollViewBottomVerticalConstraint;

@end

@implementation QRGeneratorViewController

- (instancetype)initWithTitle:(NSString*)title pageURL:(NSURL*)pageURL {
  self = [super initWithNibName:nil bundle:nil];
  if (self) {
    _pageURL = pageURL;
    _pageTitle = title;
  }
  return self;
}

#pragma mark - Properties

- (UIImage*)content {
  UIEdgeInsets padding =
      UIEdgeInsetsMake(kGeneratedImagePadding, kGeneratedImagePadding,
                       kGeneratedImagePadding, kGeneratedImagePadding);
  return ImageFromView(self.stackView, self.view.backgroundColor, padding);
}

#pragma mark - UIViewController

- (void)viewDidLoad {
  [super viewDidLoad];

  self.view.backgroundColor = [UIColor colorNamed:kBackgroundColor];

  UIToolbar* topToolbar = [self createTopToolbar];
  self.topToolbar = topToolbar;
  [self.view addSubview:topToolbar];

  UIScrollView* scrollView = [[UIScrollView alloc] init];
  scrollView.translatesAutoresizingMaskIntoConstraints = NO;
  [self.view addSubview:scrollView];

  UIView* imageView = [self createImageView];
  UILabel* title = [self createTitleLabel];
  UILabel* subtitle = [self createSubtitleLabel];
  UIStackView* stackView = [[UIStackView alloc]
      initWithArrangedSubviews:@[ imageView, title, subtitle ]];
  self.stackView = stackView;
  stackView.spacing = 8;
  stackView.axis = UILayoutConstraintAxisVertical;
  stackView.translatesAutoresizingMaskIntoConstraints = NO;
  stackView.alignment = UIStackViewAlignmentCenter;
  [scrollView addSubview:stackView];

  UIView* primaryActionButton = [self createPrimaryActionButton];
  _primaryActionButton = primaryActionButton;
  [self.view addSubview:primaryActionButton];

  // Toolbar constraints to the top.
  AddSameConstraintsToSides(
      topToolbar, self.view.safeAreaLayoutGuide,
      LayoutSides::kTrailing | LayoutSides::kTop | LayoutSides::kLeading);

  // Content size of the scrollview.
  AddSameConstraintsWithInsets(stackView, scrollView,
                               NSDirectionalEdgeInsetsMake(0, 0, 20, 0));
  // Scroll View constraints to the height of its content. Can be overridden.
  NSLayoutConstraint* heightConstraint = [scrollView.heightAnchor
      constraintEqualToAnchor:scrollView.contentLayoutGuide.heightAnchor];
  // UILayoutPriorityDefaultHigh is the default priority for content
  // compression. Setting this lower avoids compressing the content of the
  // scroll view.
  heightConstraint.priority = UILayoutPriorityDefaultHigh - 1;
  heightConstraint.active = YES;

  NSLayoutConstraint* stackViewWidth = [stackView.widthAnchor
      constraintEqualToAnchor:self.view.safeAreaLayoutGuide.widthAnchor];
  stackViewWidth.priority = UILayoutPriorityRequired - 1;

  NSLayoutConstraint* lowPriorityWidthConstraint =
      [primaryActionButton.widthAnchor
          constraintEqualToConstant:kButtonMaxWidth];
  lowPriorityWidthConstraint.priority = UILayoutPriorityDefaultHigh;

  NSLayoutConstraint* scrollViewYCenter = [scrollView.centerYAnchor
      constraintEqualToAnchor:self.view.safeAreaLayoutGuide.centerYAnchor];
  scrollViewYCenter.priority = UILayoutPriorityDefaultHigh;

  [NSLayoutConstraint activateConstraints:@[
    [scrollView.topAnchor
        constraintGreaterThanOrEqualToAnchor:topToolbar.bottomAnchor],
    [scrollView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
    [scrollView.trailingAnchor
        constraintEqualToAnchor:self.view.trailingAnchor],
    scrollViewYCenter,

    [stackView.widthAnchor
        constraintLessThanOrEqualToConstant:kContentMaxWidth],
    [stackView.centerXAnchor constraintEqualToAnchor:scrollView.centerXAnchor],

    [primaryActionButton.bottomAnchor
        constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor
                       constant:-kBottomMargin],
    [primaryActionButton.leadingAnchor
        constraintGreaterThanOrEqualToAnchor:scrollView.leadingAnchor],
    [primaryActionButton.trailingAnchor
        constraintLessThanOrEqualToAnchor:scrollView.trailingAnchor],
    [primaryActionButton.centerXAnchor
        constraintEqualToAnchor:self.view.centerXAnchor],
    lowPriorityWidthConstraint

  ]];

  self.regularHeightScrollViewBottomVerticalConstraint =
      [scrollView.bottomAnchor
          constraintLessThanOrEqualToAnchor:primaryActionButton.topAnchor
                                   constant:-8];
  self.compactHeightScrollViewBottomVerticalConstraint =
      [scrollView.bottomAnchor
          constraintLessThanOrEqualToAnchor:self.view.safeAreaLayoutGuide
                                                .bottomAnchor
                                   constant:-8];
}

- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
  [super traitCollectionDidChange:previousTraitCollection];

  // Update constraints for different size classes.
  BOOL hasNewVerticalSizeClass = previousTraitCollection.verticalSizeClass !=
                                 self.traitCollection.verticalSizeClass;

  if (hasNewVerticalSizeClass) {
    [self.view setNeedsUpdateConstraints];
  }
}

- (void)updateViewConstraints {
  BOOL isVerticalCompact =
      self.traitCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact;

  [self.primaryActionButton setHidden:isVerticalCompact];

  NSLayoutConstraint* oldBottomConstraint;
  NSLayoutConstraint* newBottomConstraint;
  if (isVerticalCompact) {
    oldBottomConstraint = self.regularHeightScrollViewBottomVerticalConstraint;
    newBottomConstraint = self.compactHeightScrollViewBottomVerticalConstraint;

    // Use setItems:animated method instead of setting the items property, as
    // that causes issues with the Done button. See crbug.com/1082723
    [self.topToolbar setItems:self.compactHeightToolbarItems animated:YES];
  } else {
    oldBottomConstraint = self.compactHeightScrollViewBottomVerticalConstraint;
    newBottomConstraint = self.regularHeightScrollViewBottomVerticalConstraint;

    // Use setItems:animated method instead of setting the items property, as
    // that causes issues with the Done button. See crbug.com/1082723
    [self.topToolbar setItems:self.regularHeightToolbarItems animated:YES];
  }

  [NSLayoutConstraint deactivateConstraints:@[ oldBottomConstraint ]];
  [NSLayoutConstraint activateConstraints:@[ newBottomConstraint ]];

  // Allow toolbar to update its height based on new layout.
  [self.topToolbar invalidateIntrinsicContentSize];

  [super updateViewConstraints];
}

#pragma mark - Private Methods

- (UIImage*)createQRCodeImage {
  NSData* urlData =
      [[self.pageURL absoluteString] dataUsingEncoding:NSUTF8StringEncoding];
  return GenerateQRCode(urlData, kQRCodeImageSize);
}

// Helper to create the top toolbar.
- (UIToolbar*)createTopToolbar {
  UIToolbar* topToolbar = [[UIToolbar alloc] init];
  topToolbar.translucent = NO;
  [topToolbar setShadowImage:[[UIImage alloc] init]
          forToolbarPosition:UIBarPositionAny];
  [topToolbar setBarTintColor:[UIColor colorNamed:kBackgroundColor]];

  NSMutableArray* regularHeightItems = [[NSMutableArray alloc] init];
  NSMutableArray* compactHeightItems = [[NSMutableArray alloc] init];
  UIImage* helpImage = DefaultSymbolWithPointSize(kHelpSymbol, kSymbolSize);
  UIBarButtonItem* helpButton =
      [[UIBarButtonItem alloc] initWithImage:helpImage
                                       style:UIBarButtonItemStylePlain
                                      target:self
                                      action:@selector(didTapHelpButton)];
  [regularHeightItems addObject:helpButton];
  [compactHeightItems addObject:helpButton];

  helpButton.isAccessibilityElement = YES;
  helpButton.accessibilityLabel =
      l10n_util::GetNSString(IDS_IOS_HELP_ACCESSIBILITY_LABEL);

  // Set the help button as the left button item so it can be used as a
  // popover anchor.
  _helpButton = helpButton;

  // Add margin with help button.
  UIBarButtonItem* fixedSpacer = [[UIBarButtonItem alloc]
      initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace
                           target:nil
                           action:nil];
  fixedSpacer.width = 15.0f;
  [compactHeightItems addObject:fixedSpacer];

  UIBarButtonItem* primaryActionBarButton = [[UIBarButtonItem alloc]
      initWithBarButtonSystemItem:UIBarButtonSystemItemAction
                           target:self
                           action:@selector(didTapPrimaryActionButton)];

  // Only shows up in constraint height mode.
  [compactHeightItems addObject:primaryActionBarButton];

  UIBarButtonItem* spacer = [[UIBarButtonItem alloc]
      initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
                           target:nil
                           action:nil];
  [regularHeightItems addObject:spacer];
  [compactHeightItems addObject:spacer];

  UIBarButtonItem* dismissButton = [[UIBarButtonItem alloc]
      initWithBarButtonSystemItem:UIBarButtonSystemItemDone
                           target:self
                           action:@selector(didTapDismissBarButton)];
  [regularHeightItems addObject:dismissButton];
  [compactHeightItems addObject:dismissButton];

  topToolbar.translatesAutoresizingMaskIntoConstraints = NO;

  self.regularHeightToolbarItems = regularHeightItems;
  self.compactHeightToolbarItems = compactHeightItems;

  return topToolbar;
}

// Handles taps on the dismiss button.
- (void)didTapDismissBarButton {
  if ([self.actionHandler
          respondsToSelector:@selector(confirmationAlertDismissAction)]) {
    [self.actionHandler confirmationAlertDismissAction];
  }
}

// Handles taps on the help button.
- (void)didTapHelpButton {
  if ([self.actionHandler
          respondsToSelector:@selector(confirmationAlertLearnMoreAction)]) {
    [self.actionHandler confirmationAlertLearnMoreAction];
  }
}

// Handles taps on the primary action button.
- (void)didTapPrimaryActionButton {
  [self.actionHandler confirmationAlertPrimaryAction];
}

// Helper to create the image view.
- (UIImageView*)createImageView {
  UIImageView* imageView =
      [[UIImageView alloc] initWithImage:[self createQRCodeImage]];
  imageView.contentMode = UIViewContentModeScaleAspectFit;

  imageView.isAccessibilityElement = YES;
  imageView.accessibilityLabel =
      l10n_util::GetNSString(IDS_IOS_QR_CODE_ACCESSIBILITY_LABEL);

  imageView.translatesAutoresizingMaskIntoConstraints = NO;
  return imageView;
}

// Helper to create the title label.
- (UILabel*)createTitleLabel {
  UILabel* title = [[UILabel alloc] init];
  title.numberOfLines = 0;
  UIFontDescriptor* descriptor = [UIFontDescriptor
      preferredFontDescriptorWithTextStyle:UIFontTextStyleTitle3];
  UIFont* font = [UIFont systemFontOfSize:descriptor.pointSize
                                   weight:UIFontWeightBold];
  UIFontMetrics* fontMetrics =
      [UIFontMetrics metricsForTextStyle:UIFontTextStyleTitle3];
  title.font = [fontMetrics scaledFontForFont:font];
  title.textColor = [UIColor colorNamed:kTextPrimaryColor];
  title.text = self.pageTitle;
  title.textAlignment = NSTextAlignmentCenter;
  title.translatesAutoresizingMaskIntoConstraints = NO;
  title.adjustsFontForContentSizeCategory = YES;
  return title;
}

// Helper to create the subtitle label.
- (UILabel*)createSubtitleLabel {
  UILabel* subtitle = [[UILabel alloc] init];
  subtitle.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
  subtitle.numberOfLines = 0;
  subtitle.textColor = [UIColor colorNamed:kTextSecondaryColor];
  subtitle.text = [self.pageURL host];
  subtitle.textAlignment = NSTextAlignmentCenter;
  subtitle.translatesAutoresizingMaskIntoConstraints = NO;
  subtitle.adjustsFontForContentSizeCategory = YES;
  return subtitle;
}

// Helper to create the primary action button.
- (UIButton*)createPrimaryActionButton {
  UIButton* primaryActionButton = PrimaryActionButton(YES);
  [primaryActionButton addTarget:self
                          action:@selector(didTapPrimaryActionButton)
                forControlEvents:UIControlEventTouchUpInside];
  SetConfigurationTitle(primaryActionButton,
                        l10n_util::GetNSString(IDS_IOS_SHARE_BUTTON_LABEL));
  [primaryActionButton
      setContentHuggingPriority:UILayoutPriorityDefaultHigh + 1
                        forAxis:UILayoutConstraintAxisVertical];

  return primaryActionButton;
}

@end