// Copyright 2023 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/tab_switcher/tab_grid/tab_groups/create_tab_group_view_controller.h"
#import "base/check.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/strings/sys_string_conversions.h"
#import "components/strings/grit/components_strings.h"
#import "components/tab_groups/tab_group_color.h"
#import "ios/chrome/browser/keyboard/ui_bundled/UIKeyCommand+Chrome.h"
#import "ios/chrome/browser/shared/model/web_state_list/tab_group.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/elements/top_aligned_image_view.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/group_tab_info.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/create_or_edit_tab_group_view_controller_delegate.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/group_tab_view.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/tab_group_creation_mutator.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/tab_group_snapshots_view.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/tab_groups_constants.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/device_form_factor.h"
#import "ui/base/l10n/l10n_util.h"
namespace {
// All elements.
const CGFloat kMaxHeight = 600;
// View constants.
const CGFloat kHorizontalMargin = 32;
const CGFloat kDotAndFieldContainerMargin = 24;
const CGFloat kDotTitleSeparationMargin = 12;
const CGFloat kContainersMaxWidth = 400;
const CGFloat kBackgroundAlpha = 0.7;
const CGFloat kCompactButtonTopMargin = 12;
const CGFloat kDotAndFieldContainerWidthPercentage = 0.5;
// Group color selection constants.
const CGFloat kColoredButtonSize = 24;
const CGFloat kColoredButtonContentInset = 8;
const CGFloat kColoredButtonWidthAndHeight = 40;
const CGFloat kColorListBottomMargin = 16;
const CGFloat kColorListBottomMarginCompact = 8;
const CGFloat kColoredDotSize = 21;
// Snapshot view constants.
const CGFloat kSnapshotViewRatio = 0.83;
const CGFloat kSnapshotViewMaxHeight = 190;
const CGFloat kSnapshotViewCornerRadius = 18;
const CGFloat kSnapshotViewVerticalMargin = 24;
const CGFloat kSingleSnapshotRatio = 0.7;
const CGFloat kMultipleSnapshotsRatio = 0.90;
const CGFloat kSnapshotViewAnimationTime = 0.3;
// Group title constants.
const CGFloat kTitleHorizontalMargin = 16;
const CGFloat kTitleVerticalMargin = 10;
const CGFloat kTitleBackgroundCornerRadius = 17;
// Button constants.
const CGFloat kButtonsHeight = 50;
const CGFloat kButtonsMargin = 8;
const CGFloat kButtonBackgroundCornerRadius = 15;
// Clear button constants.
const CGFloat kClearButtonAlpha = 0.34;
const CGFloat kClearButtonSize = 20;
const CGFloat kClearButtonWidthAndHeight = 40;
} // namespace
// Container view for the containing the snapshot. This is here to hide itself
// if it becomes too small or in compact height. The ViewController
// callbacks aren't triggered when the keyboard is shown, so it is
// necessary to subclass UIView.
@interface CreateTabGroupSnapshotContainerView : UIView
@end
@implementation CreateTabGroupSnapshotContainerView
- (void)layoutSubviews {
[super layoutSubviews];
BOOL tooSmall = self.frame.size.height < 100;
BOOL isVerticallyCompacted =
self.traitCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact;
// The snapshots container should not be displayed in the following
// scenarios:
// - When the container lacks sufficient space.
// - On devices with a vertically compact form factor.
CGFloat updatedAlpha = (tooSmall || isVerticallyCompacted) ? 0 : 1;
if (self.alpha == updatedAlpha) {
return;
}
self.alpha = updatedAlpha;
}
@end
@implementation CreateTabGroupViewController {
// Text input to name the group.
UITextField* _tabGroupTextField;
// List of all colored buttons.
NSArray<UIButton*>* _colorSelectionButtons;
// Currently selected color view represented by the dot next to the title.
UIView* _dotView;
// Currently selected colored button.
UIButton* _selectedButton;
// Default color.
tab_groups::TabGroupColorId _defaultColor;
// List of tab group pictures.
NSArray<GroupTabInfo*>* _tabGroupInfos;
// Snapshots views container.
UIView* _snapshotsContainer;
// Whether it is to edit a group (vs creation).
BOOL _editMode;
// Whether the user is syncing tabs.
BOOL _tabSynced;
// Number of selected items.
NSInteger _numberOfSelectedItems;
// Title of the group.
NSString* _title;
// Configured view that handle the snapshots dispositions.
TabGroupSnapshotsView* _snapshotsView;
// Constraints for the `_snapshotsView`, depending on if multiple snapshots
// are displayed.
NSArray<NSLayoutConstraint*>* _singleSnapshotConstraints;
NSArray<NSLayoutConstraint*>* _multipleSnapshotsConstraints;
// Buttons to create or cancel the group creation.
UIButton* _creationButton;
UIButton* _cancelButton;
UIButton* _creationButtonCompact;
UIButton* _cancelButtonCompact;
// Constraints for portrait or landscape mode.
NSArray<NSLayoutConstraint*>* _compactConstraints;
NSArray<NSLayoutConstraint*>* _regularConstraints;
// Scrollview that containts color selection buttons.
UIScrollView* _colorsScrollView;
}
- (instancetype)initWithEditMode:(BOOL)editMode
tabSynced:(BOOL)tabSynced {
CHECK(IsTabGroupInGridEnabled())
<< "You should not be able to create a tab group outside the Tab Groups "
"experiment.";
self = [super init];
if (self) {
_editMode = editMode;
_tabSynced = tabSynced;
[self createColorSelectionButtons];
CHECK_NE([_colorSelectionButtons count], 0u)
<< "The available color list for tab group is empty.";
}
return self;
}
#pragma mark - UIViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.accessibilityIdentifier = kCreateTabGroupViewIdentifier;
__weak CreateTabGroupViewController* weakSelf = self;
auto selectedDefaultButtonTest =
^BOOL(UIButton* button, NSUInteger index, BOOL* stop) {
return [weakSelf isDefaultButton:button];
};
NSInteger defaultButtonIndex = [_colorSelectionButtons
indexOfObjectPassingTest:selectedDefaultButtonTest];
_selectedButton = _colorSelectionButtons[defaultButtonIndex];
[_selectedButton setSelected:YES];
[self createConfigurations];
[self updateViews:self.view previousTraitCollection:nil];
if (@available(iOS 17, *)) {
[self registerForTraitChanges:@[ UITraitVerticalSizeClass.self ]
withAction:@selector(updateViews:
previousTraitCollection:)];
}
// To force display the keyboard when the view is shown.
[_tabGroupTextField becomeFirstResponder];
}
- (UIStatusBarStyle)preferredStatusBarStyle {
return UIStatusBarStyleLightContent;
}
- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
if (@available(iOS 17, *)) {
return;
}
if (self.traitCollection.verticalSizeClass !=
previousTraitCollection.verticalSizeClass) {
[self updateViews:self.view
previousTraitCollection:previousTraitCollection];
}
}
#pragma mark - Private helpers
// Configures the text input dedicated for the group name.
- (UITextField*)configuredTabGroupNameTextFieldInput {
UITextField* tabGroupTextField = [[UITextField alloc] init];
tabGroupTextField.textColor = [UIColor colorNamed:kSolidBlackColor];
tabGroupTextField.font =
[UIFont preferredFontForTextStyle:UIFontTextStyleTitle1];
tabGroupTextField.adjustsFontForContentSizeCategory = YES;
tabGroupTextField.translatesAutoresizingMaskIntoConstraints = NO;
tabGroupTextField.autocorrectionType = UITextAutocorrectionTypeNo;
tabGroupTextField.spellCheckingType = UITextSpellCheckingTypeNo;
tabGroupTextField.maximumContentSizeCategory =
UIContentSizeCategoryAccessibilityExtraLarge;
UIButton* clearButton = [UIButton buttonWithType:UIButtonTypeSystem];
clearButton.translatesAutoresizingMaskIntoConstraints = NO;
[clearButton setImage:DefaultSymbolWithPointSize(kXMarkCircleFillSymbol,
kClearButtonSize)
forState:UIControlStateNormal];
[clearButton setTintColor:[[UIColor colorNamed:kSolidBlackColor]
colorWithAlphaComponent:kClearButtonAlpha]];
clearButton.accessibilityLabel =
l10n_util::GetNSString(IDS_IOS_ACCNAME_CLEAR_TEXT);
clearButton.accessibilityIdentifier =
kCreateTabGroupTextFieldClearButtonIdentifier;
[clearButton addTarget:self
action:@selector(clearTextField)
forControlEvents:UIControlEventTouchUpInside];
NSLayoutConstraint* buttonWidth = [clearButton.widthAnchor
constraintEqualToConstant:kClearButtonWidthAndHeight];
buttonWidth.priority = UILayoutPriorityDefaultHigh;
[NSLayoutConstraint activateConstraints:@[
buttonWidth,
[clearButton.heightAnchor constraintEqualToAnchor:clearButton.widthAnchor],
]];
// Assign the overlay button to the text field
tabGroupTextField.rightView = clearButton;
tabGroupTextField.rightViewMode = UITextFieldViewModeWhileEditing;
tabGroupTextField.accessibilityIdentifier =
kCreateTabGroupTextFieldIdentifier;
tabGroupTextField.text = _title;
[tabGroupTextField addTarget:self
action:@selector(creationButtonTapped)
forControlEvents:UIControlEventPrimaryActionTriggered];
UIColor* placeholderTextColor = [UIColor colorNamed:kTextSecondaryColor];
tabGroupTextField.attributedPlaceholder = [[NSAttributedString alloc]
initWithString:l10n_util::GetNSString(
IDS_IOS_TAB_GROUP_CREATION_PLACEHOLDER)
attributes:@{NSForegroundColorAttributeName : placeholderTextColor}];
return tabGroupTextField;
}
// Removes text in the text field.
- (void)clearTextField {
_tabGroupTextField.text = @"";
}
// Returns the group color dot view.
- (UIView*)groupDotViewWithColor:(UIColor*)color {
UIView* dotView = [[UIView alloc] initWithFrame:CGRectZero];
dotView.translatesAutoresizingMaskIntoConstraints = NO;
dotView.layer.cornerRadius = kColoredDotSize / 2;
dotView.backgroundColor = color;
[NSLayoutConstraint activateConstraints:@[
[dotView.heightAnchor constraintEqualToConstant:kColoredDotSize],
[dotView.widthAnchor constraintEqualToConstant:kColoredDotSize],
]];
return dotView;
}
// Returns the configured full primary title (colored dot and text title).
- (UIView*)configuredDotAndFieldContainer {
UIView* titleBackground = [[UIView alloc] initWithFrame:CGRectZero];
titleBackground.translatesAutoresizingMaskIntoConstraints = NO;
titleBackground.backgroundColor = [[UIColor colorNamed:kSolidWhiteColor]
colorWithAlphaComponent:kBackgroundAlpha];
titleBackground.layer.cornerRadius = kTitleBackgroundCornerRadius;
titleBackground.opaque = NO;
tab_groups::TabGroupColorId colorID =
static_cast<tab_groups::TabGroupColorId>(_selectedButton.tag);
UIColor* defaultColor = TabGroup::ColorForTabGroupColorId(colorID);
_dotView = [self groupDotViewWithColor:defaultColor];
_tabGroupTextField = [self configuredTabGroupNameTextFieldInput];
[titleBackground addSubview:_dotView];
[titleBackground addSubview:_tabGroupTextField];
[NSLayoutConstraint activateConstraints:@[
[_tabGroupTextField.leadingAnchor
constraintEqualToAnchor:_dotView.trailingAnchor
constant:kDotTitleSeparationMargin],
[_dotView.centerYAnchor
constraintEqualToAnchor:_tabGroupTextField.centerYAnchor],
[_dotView.leadingAnchor
constraintEqualToAnchor:titleBackground.leadingAnchor
constant:kTitleHorizontalMargin],
[titleBackground.trailingAnchor
constraintEqualToAnchor:_tabGroupTextField.trailingAnchor
constant:kTitleHorizontalMargin],
[_tabGroupTextField.topAnchor
constraintEqualToAnchor:titleBackground.topAnchor
constant:kTitleVerticalMargin],
[titleBackground.bottomAnchor
constraintEqualToAnchor:_tabGroupTextField.bottomAnchor
constant:kTitleVerticalMargin],
]];
return titleBackground;
}
// Returns the cancel button.
- (UIButton*)configuredCancelButtonCompacted:(BOOL)isCompact {
UIButton* cancelButton = [UIButton buttonWithType:UIButtonTypeSystem];
cancelButton.translatesAutoresizingMaskIntoConstraints = NO;
UIColor* textColor = isCompact ? [UIColor colorNamed:kBlue600Color]
: [UIColor colorNamed:kSolidBlackColor];
NSDictionary* attributes = @{
NSFontAttributeName :
[UIFont preferredFontForTextStyle:UIFontTextStyleBody],
NSForegroundColorAttributeName : textColor
};
NSMutableAttributedString* attributedString =
[[NSMutableAttributedString alloc]
initWithString:l10n_util::GetNSString(IDS_CANCEL)
attributes:attributes];
UIButtonConfiguration* buttonConfiguration =
[UIButtonConfiguration plainButtonConfiguration];
buttonConfiguration.attributedTitle = attributedString;
cancelButton.configuration = buttonConfiguration;
[cancelButton setAttributedTitle:attributedString
forState:UIControlStateNormal];
cancelButton.maximumContentSizeCategory =
UIContentSizeCategoryExtraExtraLarge;
cancelButton.accessibilityIdentifier = kCreateTabGroupCancelButtonIdentifier;
[cancelButton addTarget:self
action:@selector(cancelButtonTapped)
forControlEvents:UIControlEventTouchUpInside];
[NSLayoutConstraint activateConstraints:@[
[cancelButton.heightAnchor
constraintGreaterThanOrEqualToConstant:kButtonsHeight],
]];
return cancelButton;
}
// Returns the cancel button.
- (UIButton*)configuredCreateGroupButtonCompacted:(BOOL)isCompact {
UIButton* creationButton = [UIButton buttonWithType:UIButtonTypeSystem];
creationButton.translatesAutoresizingMaskIntoConstraints = NO;
UIColor* textColor = isCompact ? [UIColor colorNamed:kBlue600Color]
: [UIColor colorNamed:kSolidWhiteColor];
UIFontDescriptor* boldDescriptor = [[UIFontDescriptor
preferredFontDescriptorWithTextStyle:UIFontTextStyleBody]
fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold];
UIFont* fontAttribute = [UIFont fontWithDescriptor:boldDescriptor size:0.0];
NSDictionary* attributes = @{
NSFontAttributeName : fontAttribute,
NSForegroundColorAttributeName : textColor
};
NSMutableAttributedString* attributedString =
[[NSMutableAttributedString alloc]
initWithString:_editMode ? l10n_util::GetNSString(
IDS_IOS_TAB_GROUP_CREATION_DONE)
: l10n_util::GetNSString(
IDS_IOS_TAB_GROUP_CREATION_BUTTON)
attributes:attributes];
if (isCompact) {
// The compact button adheres to the style of the iOS system buttons.
UIButtonConfiguration* buttonConfiguration =
[UIButtonConfiguration plainButtonConfiguration];
buttonConfiguration.titleLineBreakMode = NSLineBreakByWordWrapping;
buttonConfiguration.attributedTitle = attributedString;
creationButton.configuration = buttonConfiguration;
} else {
UIButtonConfiguration* buttonConfiguration =
[UIButtonConfiguration filledButtonConfiguration];
buttonConfiguration.baseBackgroundColor =
[UIColor colorNamed:kBlue600Color];
buttonConfiguration.background.cornerRadius = kButtonBackgroundCornerRadius;
buttonConfiguration.attributedTitle = attributedString;
creationButton.configuration = buttonConfiguration;
}
creationButton.maximumContentSizeCategory =
UIContentSizeCategoryExtraExtraLarge;
creationButton.accessibilityIdentifier =
kCreateTabGroupCreateButtonIdentifier;
[creationButton addTarget:self
action:@selector(creationButtonTapped)
forControlEvents:UIControlEventTouchUpInside];
[NSLayoutConstraint activateConstraints:@[
[creationButton.heightAnchor
constraintGreaterThanOrEqualToConstant:kButtonsHeight],
]];
return creationButton;
}
// Hides the current view without doing anything else.
- (void)cancelButtonTapped {
if (_editMode) {
base::RecordAction(
base::UserMetricsAction("MobileTabGroupUserCanceledGroupEdition"));
} else {
base::RecordAction(
base::UserMetricsAction("MobileTabGroupUserCanceledNewGroupCreation"));
}
[self dismissViewController];
}
// Creates the group and dismiss the view.
- (void)creationButtonTapped {
__weak CreateTabGroupViewController* weakSelf = self;
[self.mutator
createNewGroupWithTitle:_tabGroupTextField.text
color:static_cast<tab_groups::TabGroupColorId>(
_selectedButton.tag)
completion:^{
[weakSelf dismissViewController];
}];
}
// Dismisses the current view.
- (void)dismissViewController {
// Hide elements attached to the keyboard for dismissing the view. The
// keyboard dismissing animation is longer than the view one, and elements
// attached to the keyboard are still visible for a frame after the end of the
// view animation.
_cancelButton.hidden = YES;
_creationButton.hidden = YES;
_colorsScrollView.hidden = YES;
[self.delegate createOrEditTabGroupViewControllerDidDismiss:self
animated:YES];
}
// Changes the selected color.
- (void)coloredButtonTapped:(UIButton*)sender {
if (sender.isSelected) {
return;
}
[_selectedButton setSelected:NO];
_selectedButton = sender;
[_selectedButton setSelected:YES];
tab_groups::TabGroupColorId colorID =
static_cast<tab_groups::TabGroupColorId>(_selectedButton.tag);
[_dotView setBackgroundColor:TabGroup::ColorForTabGroupColorId(colorID)];
}
// Creates all the available color buttons.
- (void)createColorSelectionButtons {
NSMutableArray* buttons = [[NSMutableArray alloc] init];
const tab_groups::ColorLabelMap colorLabelMap =
tab_groups::GetTabGroupColorLabelMap();
for (tab_groups::TabGroupColorId colorID :
TabGroup::AllPossibleTabGroupColors()) {
UIButton* colorButton = [[UIButton alloc] init];
colorButton.translatesAutoresizingMaskIntoConstraints = NO;
[colorButton setTag:static_cast<NSInteger>(colorID)];
UIButtonConfiguration* buttonConfiguration =
[UIButtonConfiguration plainButtonConfiguration];
buttonConfiguration.baseBackgroundColor = [UIColor clearColor];
buttonConfiguration.contentInsets = NSDirectionalEdgeInsets(
kColoredButtonContentInset, kColoredButtonContentInset,
kColoredButtonContentInset, kColoredButtonContentInset);
colorButton.configuration = buttonConfiguration;
colorButton.accessibilityLabel = l10n_util::GetNSStringF(
IDS_IOS_TAB_GROUP_CREATION_ACCESSIBILITY_COLOR_SELECTION,
colorLabelMap.at(colorID));
UIImageSymbolConfiguration* configuration = [UIImageSymbolConfiguration
configurationWithPointSize:kColoredButtonSize
weight:UIImageSymbolWeightRegular
scale:UIImageSymbolScaleDefault];
UIImage* normalSymbolImage =
DefaultSymbolWithConfiguration(kCircleFillSymbol, configuration);
normalSymbolImage = [normalSymbolImage
imageWithTintColor:TabGroup::ColorForTabGroupColorId(colorID)
renderingMode:UIImageRenderingModeAlwaysOriginal];
UIImage* selectedSymbolImage =
DefaultSymbolWithConfiguration(kCircleCircleFillSymbol, configuration);
selectedSymbolImage = [selectedSymbolImage
imageWithTintColor:TabGroup::ColorForTabGroupColorId(colorID)
renderingMode:UIImageRenderingModeAlwaysOriginal];
[colorButton setImage:normalSymbolImage forState:UIControlStateNormal];
[colorButton setImage:selectedSymbolImage forState:UIControlStateSelected];
[colorButton addTarget:self
action:@selector(coloredButtonTapped:)
forControlEvents:UIControlEventTouchUpInside];
[NSLayoutConstraint activateConstraints:@[
[colorButton.widthAnchor
constraintEqualToConstant:kColoredButtonWidthAndHeight],
[colorButton.heightAnchor
constraintEqualToAnchor:colorButton.widthAnchor],
]];
[buttons addObject:colorButton];
}
_colorSelectionButtons = buttons;
}
// Returns the configured view, which contains all the available colors.
- (UIScrollView*)listOfColorView {
UIStackView* colorsView = [[UIStackView alloc] init];
colorsView.alignment = UIStackViewAlignmentTop;
colorsView.translatesAutoresizingMaskIntoConstraints = NO;
UIScrollView* scrollView = [[UIScrollView alloc] init];
scrollView.translatesAutoresizingMaskIntoConstraints = NO;
scrollView.canCancelContentTouches = YES;
[scrollView setShowsHorizontalScrollIndicator:NO];
[scrollView setShowsVerticalScrollIndicator:NO];
[scrollView addSubview:colorsView];
for (UIButton* button in _colorSelectionButtons) {
[colorsView addArrangedSubview:button];
}
[self updateScrollViewInsets:scrollView];
NSLayoutConstraint* scrollViewWidthConstraint = [scrollView.widthAnchor
constraintGreaterThanOrEqualToAnchor:colorsView.widthAnchor];
scrollViewWidthConstraint.priority = UILayoutPriorityDefaultLow;
AddSameConstraints(scrollView.contentLayoutGuide, colorsView);
[NSLayoutConstraint activateConstraints:@[
[scrollView.heightAnchor constraintEqualToAnchor:colorsView.heightAnchor],
scrollViewWidthConstraint,
]];
return scrollView;
}
// YES if the given button is the default one.
- (BOOL)isDefaultButton:(UIButton*)button {
return static_cast<tab_groups::TabGroupColorId>(button.tag) == _defaultColor;
}
// Updates all the view and subviews depending on space available.
- (void)updateViews:(UIView*)updatedView
previousTraitCollection:(UITraitCollection*)previousTraitCollection {
if (previousTraitCollection.verticalSizeClass ==
self.traitCollection.verticalSizeClass) {
return;
}
NSArray<UIView*>* toHide;
NSArray<UIView*>* toDisplay;
BOOL isVerticallyCompacted =
self.traitCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact;
if (isVerticallyCompacted) {
toHide = @[ _cancelButton, _creationButton ];
toDisplay = @[ _cancelButtonCompact, _creationButtonCompact ];
[NSLayoutConstraint deactivateConstraints:_regularConstraints];
[NSLayoutConstraint activateConstraints:_compactConstraints];
} else {
toHide = @[ _cancelButtonCompact, _creationButtonCompact ];
toDisplay = @[ _cancelButton, _creationButton ];
[NSLayoutConstraint deactivateConstraints:_compactConstraints];
[NSLayoutConstraint activateConstraints:_regularConstraints];
}
for (UIView* view in toDisplay) {
view.hidden = NO;
view.alpha = 0;
}
UIScrollView* scrollView = _colorsScrollView;
__weak __typeof(self) weakSelf = self;
[UIView animateWithDuration:kSnapshotViewAnimationTime
animations:^{
for (UIView* view in toHide) {
view.alpha = 0;
}
for (UIView* view in toDisplay) {
view.alpha = 1;
}
[weakSelf updateScrollViewInsets:scrollView];
[weakSelf.view layoutIfNeeded];
}
completion:^(BOOL finished) {
for (UIView* view in toHide) {
view.hidden = YES;
}
}];
// To force display the keyboard.
[_tabGroupTextField becomeFirstResponder];
}
// Configures the view and all subviews when there is enough space.
- (void)createConfigurations {
UIView* dotAndFieldContainer = [self configuredDotAndFieldContainer];
UILayoutGuide* snapshotsContainerLayoutGuide = [[UILayoutGuide alloc] init];
_snapshotsContainer = [self configuredSnapshotsContainer];
_colorsScrollView = [self listOfColorView];
_creationButton = [self configuredCreateGroupButtonCompacted:NO];
_cancelButton = [self configuredCancelButtonCompacted:NO];
_creationButtonCompact = [self configuredCreateGroupButtonCompacted:YES];
_cancelButtonCompact = [self configuredCancelButtonCompacted:YES];
UIView* container = [[UIView alloc] init];
container.translatesAutoresizingMaskIntoConstraints = NO;
[container addSubview:dotAndFieldContainer];
[container addSubview:_snapshotsContainer];
[container addLayoutGuide:snapshotsContainerLayoutGuide];
[container addSubview:_colorsScrollView];
[container addSubview:_cancelButtonCompact];
[container addSubview:_creationButtonCompact];
[container addSubview:_cancelButton];
[container addSubview:_creationButton];
[self.view addSubview:container];
NSLayoutConstraint* keyboardConstraint = [container.bottomAnchor
constraintEqualToAnchor:self.view.keyboardLayoutGuide.topAnchor];
keyboardConstraint.priority = UILayoutPriorityDefaultHigh + 1;
NSLayoutConstraint* snapshotLayoutGuideConstraint =
[snapshotsContainerLayoutGuide.bottomAnchor
constraintEqualToAnchor:_colorsScrollView.topAnchor
constant:-kSnapshotViewVerticalMargin];
snapshotLayoutGuideConstraint.priority = UILayoutPriorityDefaultHigh + 1;
_regularConstraints = @[
[dotAndFieldContainer.leadingAnchor
constraintGreaterThanOrEqualToAnchor:container.leadingAnchor
constant:kHorizontalMargin],
[dotAndFieldContainer.trailingAnchor
constraintLessThanOrEqualToAnchor:container.trailingAnchor
constant:-kHorizontalMargin],
[_creationButton.widthAnchor
constraintEqualToAnchor:dotAndFieldContainer.widthAnchor],
[_cancelButton.widthAnchor
constraintEqualToAnchor:dotAndFieldContainer.widthAnchor],
[_cancelButton.bottomAnchor constraintEqualToAnchor:container.bottomAnchor
constant:-kButtonsMargin],
];
_compactConstraints = @[
[dotAndFieldContainer.widthAnchor
constraintLessThanOrEqualToAnchor:self.view.widthAnchor
multiplier:kDotAndFieldContainerWidthPercentage],
[_cancelButtonCompact.trailingAnchor
constraintLessThanOrEqualToAnchor:dotAndFieldContainer.leadingAnchor],
[_creationButtonCompact.leadingAnchor
constraintGreaterThanOrEqualToAnchor:dotAndFieldContainer
.trailingAnchor],
[_colorsScrollView.bottomAnchor
constraintEqualToAnchor:container.bottomAnchor
constant:-kColorListBottomMarginCompact],
];
NSLayoutConstraint* dotAndFieldWidth = [dotAndFieldContainer.widthAnchor
constraintEqualToConstant:kContainersMaxWidth];
dotAndFieldWidth.priority = UILayoutPriorityDefaultHigh;
[NSLayoutConstraint activateConstraints:@[
[dotAndFieldContainer.topAnchor
constraintEqualToAnchor:container.topAnchor
constant:kDotAndFieldContainerMargin],
[dotAndFieldContainer.heightAnchor
constraintGreaterThanOrEqualToConstant:kButtonsHeight],
dotAndFieldWidth,
[dotAndFieldContainer.centerXAnchor
constraintEqualToAnchor:self.view.centerXAnchor],
[_colorsScrollView.leadingAnchor
constraintGreaterThanOrEqualToAnchor:container.leadingAnchor],
[_colorsScrollView.trailingAnchor
constraintLessThanOrEqualToAnchor:container.trailingAnchor],
[_colorsScrollView.centerXAnchor
constraintEqualToAnchor:self.view.centerXAnchor],
[container.topAnchor
constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor],
[container.leadingAnchor
constraintEqualToAnchor:self.view.safeAreaLayoutGuide.leadingAnchor],
[container.trailingAnchor
constraintEqualToAnchor:self.view.safeAreaLayoutGuide.trailingAnchor],
[container.heightAnchor constraintLessThanOrEqualToConstant:kMaxHeight],
[_creationButton.centerXAnchor
constraintEqualToAnchor:self.view.centerXAnchor],
[_cancelButton.centerXAnchor
constraintEqualToAnchor:self.view.centerXAnchor],
[_creationButton.bottomAnchor
constraintEqualToAnchor:_cancelButton.topAnchor],
[_creationButton.topAnchor
constraintEqualToAnchor:_colorsScrollView.bottomAnchor
constant:kColorListBottomMargin],
[_cancelButtonCompact.leadingAnchor
constraintEqualToAnchor:container.leadingAnchor],
[_creationButtonCompact.trailingAnchor
constraintEqualToAnchor:container.trailingAnchor],
[_cancelButtonCompact.topAnchor
constraintEqualToAnchor:container.topAnchor
constant:kCompactButtonTopMargin],
[_creationButtonCompact.topAnchor
constraintEqualToAnchor:container.topAnchor
constant:kCompactButtonTopMargin],
[snapshotsContainerLayoutGuide.centerXAnchor
constraintEqualToAnchor:self.view.centerXAnchor],
[snapshotsContainerLayoutGuide.topAnchor
constraintEqualToAnchor:dotAndFieldContainer.bottomAnchor
constant:kSnapshotViewVerticalMargin],
[snapshotsContainerLayoutGuide.widthAnchor
constraintEqualToAnchor:dotAndFieldContainer.widthAnchor],
snapshotLayoutGuideConstraint,
[_snapshotsContainer.centerXAnchor
constraintEqualToAnchor:snapshotsContainerLayoutGuide.centerXAnchor],
[_snapshotsContainer.centerYAnchor
constraintEqualToAnchor:snapshotsContainerLayoutGuide.centerYAnchor],
[_snapshotsContainer.heightAnchor
constraintLessThanOrEqualToAnchor:snapshotsContainerLayoutGuide
.heightAnchor],
[_snapshotsContainer.widthAnchor
constraintLessThanOrEqualToAnchor:snapshotsContainerLayoutGuide
.widthAnchor],
keyboardConstraint,
]];
}
// Returns the view which contains all the selected tabs' snapshot which will be
// included in the tab group.
- (UIView*)configuredSnapshotsContainer {
UIView* snapshotsBackground =
[[CreateTabGroupSnapshotContainerView alloc] init];
snapshotsBackground.translatesAutoresizingMaskIntoConstraints = NO;
snapshotsBackground.backgroundColor = [[UIColor colorNamed:kSolidWhiteColor]
colorWithAlphaComponent:kBackgroundAlpha];
snapshotsBackground.layer.cornerRadius = kSnapshotViewCornerRadius;
snapshotsBackground.opaque = NO;
_snapshotsView = [[TabGroupSnapshotsView alloc]
initWithTabGroupInfos:_tabGroupInfos
size:_numberOfSelectedItems
light:self.traitCollection.userInterfaceStyle ==
UIUserInterfaceStyleLight
cell:NO];
[snapshotsBackground addSubview:_snapshotsView];
NSLayoutConstraint* backgroundHeightConstraint =
[snapshotsBackground.heightAnchor
constraintEqualToConstant:kSnapshotViewMaxHeight];
backgroundHeightConstraint.priority = UILayoutPriorityDefaultHigh;
_singleSnapshotConstraints = @[
[_snapshotsView.widthAnchor
constraintEqualToAnchor:snapshotsBackground.widthAnchor
multiplier:kSingleSnapshotRatio],
[_snapshotsView.heightAnchor
constraintEqualToAnchor:snapshotsBackground.heightAnchor
multiplier:kSingleSnapshotRatio]
];
_multipleSnapshotsConstraints = @[
[_snapshotsView.widthAnchor
constraintEqualToAnchor:snapshotsBackground.widthAnchor
multiplier:kMultipleSnapshotsRatio],
[_snapshotsView.heightAnchor
constraintEqualToAnchor:snapshotsBackground.heightAnchor
multiplier:kMultipleSnapshotsRatio]
];
[NSLayoutConstraint activateConstraints:@[
backgroundHeightConstraint,
[snapshotsBackground.widthAnchor
constraintEqualToAnchor:snapshotsBackground.heightAnchor
multiplier:kSnapshotViewRatio],
[_snapshotsView.centerXAnchor
constraintEqualToAnchor:snapshotsBackground.centerXAnchor],
[_snapshotsView.centerYAnchor
constraintEqualToAnchor:snapshotsBackground.centerYAnchor],
]];
[self applyConstraints];
return snapshotsBackground;
}
#pragma mark - TabGroupCreationConsumer
- (void)setDefaultGroupColor:(tab_groups::TabGroupColorId)color {
_defaultColor = color;
}
- (void)setTabGroupInfos:(NSArray<GroupTabInfo*>*)tabGroupInfos
numberOfSelectedItems:(NSInteger)numberOfSelectedItems {
_tabGroupInfos = tabGroupInfos;
_numberOfSelectedItems = numberOfSelectedItems;
[_snapshotsView
configureTabGroupSnapshotsViewWithTabGroupInfos:tabGroupInfos
size:_numberOfSelectedItems];
[self applyConstraints];
}
- (void)setGroupTitle:(NSString*)title {
_title = title;
}
#pragma mark - Accessibility
- (BOOL)accessibilityPerformEscape {
[self dismissViewController];
return YES;
}
#pragma mark - UIResponder
// To always be able to register key commands via -keyCommands, the VC must be
// able to become first responder.
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (NSArray*)keyCommands {
return @[ UIKeyCommand.cr_close ];
}
- (void)keyCommand_close {
base::RecordAction(base::UserMetricsAction("MobileKeyCommandClose"));
[self dismissViewController];
}
#pragma mark - Private Helpers
// Activates or deactivates the appropriate constraints.
- (void)applyConstraints {
if (_numberOfSelectedItems == 1) {
[NSLayoutConstraint deactivateConstraints:_multipleSnapshotsConstraints];
[NSLayoutConstraint activateConstraints:_singleSnapshotConstraints];
} else {
[NSLayoutConstraint deactivateConstraints:_singleSnapshotConstraints];
[NSLayoutConstraint activateConstraints:_multipleSnapshotsConstraints];
}
}
// Updates the insets of the `colorScrollView`.
- (void)updateScrollViewInsets:(UIScrollView*)colorScrollView {
CGFloat viewWidth = self.view.safeAreaLayoutGuide.layoutFrame.size.width;
CGFloat selectionWidth =
[_colorSelectionButtons count] * kColoredButtonWidthAndHeight;
if (selectionWidth > viewWidth) {
colorScrollView.contentInset =
UIEdgeInsetsMake(0, kHorizontalMargin, 0, kHorizontalMargin);
if (colorScrollView.contentOffset.x == 0) {
colorScrollView.contentOffset = CGPointMake(-kHorizontalMargin, 0);
}
} else {
colorScrollView.contentInset = UIEdgeInsetsZero;
}
}
@end