mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-09 09:18:42 +09:00
1061 lines
43 KiB
Swift
1061 lines
43 KiB
Swift
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|||
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|||
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|||
|
|
|
|||
|
|
import Foundation
|
|||
|
|
import UIKit
|
|||
|
|
import SnapKit
|
|||
|
|
import Storage
|
|||
|
|
import ReadingList
|
|||
|
|
import Shared
|
|||
|
|
|
|||
|
|
struct TabTrayControllerUX {
|
|||
|
|
static let CornerRadius = CGFloat(6.0)
|
|||
|
|
static let BackgroundColor = UIConstants.TabTrayBG
|
|||
|
|
static let CellBackgroundColor = UIConstants.TabTrayBG
|
|||
|
|
static let TextBoxHeight = CGFloat(32.0)
|
|||
|
|
static let FaviconSize = CGFloat(20)
|
|||
|
|
static let Margin = CGFloat(15)
|
|||
|
|
static let ToolbarBarTintColor = UIColor.black
|
|||
|
|
static let ToolbarButtonOffset = CGFloat(10.0)
|
|||
|
|
static let CloseButtonSize = CGFloat(18.0)
|
|||
|
|
static let CloseButtonMargin = CGFloat(6.0)
|
|||
|
|
static let CloseButtonEdgeInset = CGFloat(10)
|
|||
|
|
|
|||
|
|
static let NumberOfColumnsThin = 1
|
|||
|
|
static let NumberOfColumnsWide = 3
|
|||
|
|
static let CompactNumberOfColumnsThin = 2
|
|||
|
|
|
|||
|
|
static let MenuFixedWidth: CGFloat = 320
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
struct LightTabCellUX {
|
|||
|
|
static let TabTitleTextColor = UIColor.black
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
struct DarkTabCellUX {
|
|||
|
|
static let TabTitleTextColor = UIColor.white
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
protocol TabCellDelegate: class {
|
|||
|
|
func tabCellDidClose(_ cell: TabCell)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class TabCell: UICollectionViewCell {
|
|||
|
|
enum Style {
|
|||
|
|
case light
|
|||
|
|
case dark
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static let Identifier = "TabCellIdentifier"
|
|||
|
|
static let BorderWidth: CGFloat = 3
|
|||
|
|
|
|||
|
|
var style: Style = .light {
|
|||
|
|
didSet {
|
|||
|
|
applyStyle(style)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let backgroundHolder = UIView()
|
|||
|
|
let screenshotView = UIImageViewAligned()
|
|||
|
|
let titleText: UILabel
|
|||
|
|
let favicon: UIImageView = UIImageView()
|
|||
|
|
let closeButton: UIButton
|
|||
|
|
|
|||
|
|
var title: UIVisualEffectView!
|
|||
|
|
var animator: SwipeAnimator!
|
|||
|
|
|
|||
|
|
weak var delegate: TabCellDelegate?
|
|||
|
|
|
|||
|
|
// Changes depending on whether we're full-screen or not.
|
|||
|
|
var margin = CGFloat(0)
|
|||
|
|
|
|||
|
|
override init(frame: CGRect) {
|
|||
|
|
self.backgroundHolder.backgroundColor = UIColor.white
|
|||
|
|
self.backgroundHolder.layer.cornerRadius = TabTrayControllerUX.CornerRadius
|
|||
|
|
self.backgroundHolder.clipsToBounds = true
|
|||
|
|
self.backgroundHolder.backgroundColor = TabTrayControllerUX.CellBackgroundColor
|
|||
|
|
|
|||
|
|
self.screenshotView.contentMode = UIViewContentMode.scaleAspectFill
|
|||
|
|
self.screenshotView.clipsToBounds = true
|
|||
|
|
self.screenshotView.isUserInteractionEnabled = false
|
|||
|
|
self.screenshotView.alignLeft = true
|
|||
|
|
self.screenshotView.alignTop = true
|
|||
|
|
screenshotView.backgroundColor = UIConstants.AppBackgroundColor
|
|||
|
|
|
|||
|
|
self.favicon.backgroundColor = UIColor.clear
|
|||
|
|
self.favicon.layer.cornerRadius = 2.0
|
|||
|
|
self.favicon.layer.masksToBounds = true
|
|||
|
|
|
|||
|
|
self.titleText = UILabel()
|
|||
|
|
self.titleText.textAlignment = NSTextAlignment.left
|
|||
|
|
self.titleText.isUserInteractionEnabled = false
|
|||
|
|
self.titleText.numberOfLines = 1
|
|||
|
|
self.titleText.font = DynamicFontHelper.defaultHelper.DefaultSmallFontBold
|
|||
|
|
|
|||
|
|
self.closeButton = UIButton()
|
|||
|
|
self.closeButton.setImage(UIImage.templateImageNamed("nav-stop"), for: UIControlState())
|
|||
|
|
self.closeButton.tintColor = UIColor.lightGray
|
|||
|
|
self.closeButton.imageEdgeInsets = UIEdgeInsets(equalInset: TabTrayControllerUX.CloseButtonEdgeInset)
|
|||
|
|
|
|||
|
|
super.init(frame: frame)
|
|||
|
|
|
|||
|
|
self.animator = SwipeAnimator(animatingView: self)
|
|||
|
|
self.closeButton.addTarget(self, action: #selector(TabCell.close), for: UIControlEvents.touchUpInside)
|
|||
|
|
|
|||
|
|
contentView.addSubview(backgroundHolder)
|
|||
|
|
backgroundHolder.addSubview(self.screenshotView)
|
|||
|
|
|
|||
|
|
// Default style is light
|
|||
|
|
applyStyle(style)
|
|||
|
|
|
|||
|
|
self.accessibilityCustomActions = [
|
|||
|
|
UIAccessibilityCustomAction(name: NSLocalizedString("Close", comment: "Accessibility label for action denoting closing a tab in tab list (tray)"), target: self.animator, selector: #selector(SwipeAnimator.closeWithoutGesture))
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fileprivate func applyStyle(_ style: Style) {
|
|||
|
|
self.title?.removeFromSuperview()
|
|||
|
|
|
|||
|
|
let title: UIVisualEffectView
|
|||
|
|
switch style {
|
|||
|
|
case .light:
|
|||
|
|
title = UIVisualEffectView(effect: UIBlurEffect(style: .extraLight))
|
|||
|
|
self.titleText.textColor = LightTabCellUX.TabTitleTextColor
|
|||
|
|
case .dark:
|
|||
|
|
title = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
|
|||
|
|
self.titleText.textColor = DarkTabCellUX.TabTitleTextColor
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
titleText.backgroundColor = UIColor.clear
|
|||
|
|
|
|||
|
|
title.contentView.addSubview(self.closeButton)
|
|||
|
|
title.contentView.addSubview(self.titleText)
|
|||
|
|
title.contentView.addSubview(self.favicon)
|
|||
|
|
|
|||
|
|
backgroundHolder.addSubview(title)
|
|||
|
|
self.title = title
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func setTabSelected(_ isPrivate: Bool) {
|
|||
|
|
// This creates a border around a tabcell. Using the shadow craetes a border _outside_ of the tab frame.
|
|||
|
|
layer.shadowColor = (isPrivate ? UIConstants.PrivateModePurple : UIConstants.SystemBlueColor).cgColor
|
|||
|
|
layer.shadowOpacity = 1
|
|||
|
|
layer.shadowRadius = 0 // A 0 radius creates a solid border instead of a gradient blur
|
|||
|
|
layer.masksToBounds = false
|
|||
|
|
// create a frame that is "BorderWidth" size bigger than the cell
|
|||
|
|
layer.shadowOffset = CGSize(width: -TabCell.BorderWidth, height: -TabCell.BorderWidth)
|
|||
|
|
let shadowPath = CGRect(x: 0, y: 0, width: layer.frame.width + (TabCell.BorderWidth * 2), height: layer.frame.height + (TabCell.BorderWidth * 2))
|
|||
|
|
layer.shadowPath = UIBezierPath(roundedRect: shadowPath, cornerRadius: TabTrayControllerUX.CornerRadius+TabCell.BorderWidth).cgPath
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
required init?(coder aDecoder: NSCoder) {
|
|||
|
|
fatalError("init(coder:) has not been implemented")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
override func layoutSubviews() {
|
|||
|
|
super.layoutSubviews()
|
|||
|
|
|
|||
|
|
let w = frame.width
|
|||
|
|
let h = frame.height
|
|||
|
|
backgroundHolder.frame = CGRect(x: margin,
|
|||
|
|
y: margin,
|
|||
|
|
width: w,
|
|||
|
|
height: h)
|
|||
|
|
screenshotView.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: backgroundHolder.frame.size)
|
|||
|
|
|
|||
|
|
title.frame = CGRect(x: 0,
|
|||
|
|
y: 0,
|
|||
|
|
width: backgroundHolder.frame.width,
|
|||
|
|
height: TabTrayControllerUX.TextBoxHeight)
|
|||
|
|
|
|||
|
|
favicon.frame = CGRect(x: 6,
|
|||
|
|
y: (TabTrayControllerUX.TextBoxHeight - TabTrayControllerUX.FaviconSize)/2,
|
|||
|
|
width: TabTrayControllerUX.FaviconSize,
|
|||
|
|
height: TabTrayControllerUX.FaviconSize)
|
|||
|
|
|
|||
|
|
let titleTextLeft = favicon.frame.origin.x + favicon.frame.width + 6
|
|||
|
|
titleText.frame = CGRect(x: titleTextLeft,
|
|||
|
|
y: 0,
|
|||
|
|
width: title.frame.width - titleTextLeft - margin - TabTrayControllerUX.CloseButtonSize - TabTrayControllerUX.CloseButtonMargin * 2,
|
|||
|
|
height: title.frame.height)
|
|||
|
|
|
|||
|
|
closeButton.snp.makeConstraints { make in
|
|||
|
|
make.size.equalTo(title.snp.height)
|
|||
|
|
make.trailing.centerY.equalTo(title)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let top = (TabTrayControllerUX.TextBoxHeight - titleText.bounds.height) / 2.0
|
|||
|
|
titleText.frame.origin = CGPoint(x: titleText.frame.origin.x, y: max(0, top))
|
|||
|
|
let shadowPath = CGRect(x: 0, y: 0, width: layer.frame.width + (TabCell.BorderWidth * 2), height: layer.frame.height + (TabCell.BorderWidth * 2))
|
|||
|
|
layer.shadowPath = UIBezierPath(roundedRect: shadowPath, cornerRadius: TabTrayControllerUX.CornerRadius+TabCell.BorderWidth).cgPath
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
override func prepareForReuse() {
|
|||
|
|
// Reset any close animations.
|
|||
|
|
backgroundHolder.transform = CGAffineTransform.identity
|
|||
|
|
backgroundHolder.alpha = 1
|
|||
|
|
self.titleText.font = DynamicFontHelper.defaultHelper.DefaultSmallFontBold
|
|||
|
|
layer.shadowOffset = CGSize.zero
|
|||
|
|
layer.shadowPath = nil
|
|||
|
|
layer.shadowOpacity = 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
override func accessibilityScroll(_ direction: UIAccessibilityScrollDirection) -> Bool {
|
|||
|
|
var right: Bool
|
|||
|
|
switch direction {
|
|||
|
|
case .left:
|
|||
|
|
right = false
|
|||
|
|
case .right:
|
|||
|
|
right = true
|
|||
|
|
default:
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
animator.close(right: right)
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@objc
|
|||
|
|
func close() {
|
|||
|
|
self.animator.closeWithoutGesture()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
struct PrivateModeStrings {
|
|||
|
|
static let toggleAccessibilityLabel = NSLocalizedString("Private Mode", tableName: "PrivateBrowsing", comment: "Accessibility label for toggling on/off private mode")
|
|||
|
|
static let toggleAccessibilityHint = NSLocalizedString("Turns private mode on or off", tableName: "PrivateBrowsing", comment: "Accessiblity hint for toggling on/off private mode")
|
|||
|
|
static let toggleAccessibilityValueOn = NSLocalizedString("On", tableName: "PrivateBrowsing", comment: "Toggled ON accessibility value")
|
|||
|
|
static let toggleAccessibilityValueOff = NSLocalizedString("Off", tableName: "PrivateBrowsing", comment: "Toggled OFF accessibility value")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
protocol TabTrayDelegate: class {
|
|||
|
|
func tabTrayDidDismiss(_ tabTray: TabTrayController)
|
|||
|
|
func tabTrayDidAddBookmark(_ tab: Tab)
|
|||
|
|
func tabTrayDidAddToReadingList(_ tab: Tab) -> ReadingListClientRecord?
|
|||
|
|
func tabTrayRequestsPresentationOf(_ viewController: UIViewController)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class TabTrayController: UIViewController {
|
|||
|
|
let tabManager: TabManager
|
|||
|
|
let profile: Profile
|
|||
|
|
weak var delegate: TabTrayDelegate?
|
|||
|
|
|
|||
|
|
var collectionView: UICollectionView!
|
|||
|
|
var draggedCell: TabCell?
|
|||
|
|
var dragOffset: CGPoint = CGPoint.zero
|
|||
|
|
lazy var toolbar: TrayToolbar = {
|
|||
|
|
let toolbar = TrayToolbar()
|
|||
|
|
toolbar.addTabButton.addTarget(self, action: #selector(TabTrayController.didClickAddTab), for: .touchUpInside)
|
|||
|
|
toolbar.maskButton.addTarget(self, action: #selector(TabTrayController.didTogglePrivateMode), for: .touchUpInside)
|
|||
|
|
toolbar.deleteButton.addTarget(self, action: #selector(TabTrayController.didTapDelete(_:)), for: .touchUpInside)
|
|||
|
|
return toolbar
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
fileprivate(set) internal var privateMode: Bool = false {
|
|||
|
|
didSet {
|
|||
|
|
tabDataSource.tabs = tabsToDisplay
|
|||
|
|
toolbar.styleToolbar(privateMode)
|
|||
|
|
collectionView?.reloadData()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fileprivate var tabsToDisplay: [Tab] {
|
|||
|
|
return self.privateMode ? tabManager.privateTabs : tabManager.normalTabs
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fileprivate lazy var emptyPrivateTabsView: EmptyPrivateTabsView = {
|
|||
|
|
let emptyView = EmptyPrivateTabsView()
|
|||
|
|
emptyView.learnMoreButton.addTarget(self, action: #selector(TabTrayController.didTapLearnMore), for: UIControlEvents.touchUpInside)
|
|||
|
|
return emptyView
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
fileprivate lazy var tabDataSource: TabManagerDataSource = {
|
|||
|
|
return TabManagerDataSource(tabs: self.tabsToDisplay, cellDelegate: self, tabManager: self.tabManager)
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
fileprivate lazy var tabLayoutDelegate: TabLayoutDelegate = {
|
|||
|
|
let delegate = TabLayoutDelegate(profile: self.profile, traitCollection: self.traitCollection)
|
|||
|
|
delegate.tabSelectionDelegate = self
|
|||
|
|
return delegate
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
init(tabManager: TabManager, profile: Profile) {
|
|||
|
|
self.tabManager = tabManager
|
|||
|
|
self.profile = profile
|
|||
|
|
super.init(nibName: nil, bundle: nil)
|
|||
|
|
|
|||
|
|
tabManager.addDelegate(self)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
convenience init(tabManager: TabManager, profile: Profile, tabTrayDelegate: TabTrayDelegate) {
|
|||
|
|
self.init(tabManager: tabManager, profile: profile)
|
|||
|
|
self.delegate = tabTrayDelegate
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
required init?(coder aDecoder: NSCoder) {
|
|||
|
|
fatalError("init(coder:) has not been implemented")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
deinit {
|
|||
|
|
self.tabManager.removeDelegate(self)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func dynamicFontChanged(_ notification: Notification) {
|
|||
|
|
guard notification.name == NotificationDynamicFontChanged else { return }
|
|||
|
|
|
|||
|
|
self.collectionView.reloadData()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MARK: View Controller Callbacks
|
|||
|
|
override func viewDidLoad() {
|
|||
|
|
super.viewDidLoad()
|
|||
|
|
|
|||
|
|
view.accessibilityLabel = NSLocalizedString("Tabs Tray", comment: "Accessibility label for the Tabs Tray view.")
|
|||
|
|
|
|||
|
|
collectionView = UICollectionView(frame: view.frame, collectionViewLayout: UICollectionViewFlowLayout())
|
|||
|
|
|
|||
|
|
collectionView.dataSource = tabDataSource
|
|||
|
|
collectionView.delegate = tabLayoutDelegate
|
|||
|
|
collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.BottomToolbarHeight, right: 0)
|
|||
|
|
collectionView.register(TabCell.self, forCellWithReuseIdentifier: TabCell.Identifier)
|
|||
|
|
collectionView.backgroundColor = TabTrayControllerUX.BackgroundColor
|
|||
|
|
|
|||
|
|
view.addSubview(collectionView)
|
|||
|
|
view.addSubview(toolbar)
|
|||
|
|
|
|||
|
|
makeConstraints()
|
|||
|
|
|
|||
|
|
view.insertSubview(emptyPrivateTabsView, aboveSubview: collectionView)
|
|||
|
|
emptyPrivateTabsView.snp.makeConstraints { make in
|
|||
|
|
make.top.left.right.equalTo(self.collectionView)
|
|||
|
|
make.bottom.equalTo(self.toolbar.snp.top)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if let tab = tabManager.selectedTab, tab.isPrivate {
|
|||
|
|
privateMode = true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// register for previewing delegate to enable peek and pop if force touch feature available
|
|||
|
|
if traitCollection.forceTouchCapability == .available {
|
|||
|
|
registerForPreviewing(with: self, sourceView: view)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
emptyPrivateTabsView.isHidden = !privateTabsAreEmpty()
|
|||
|
|
|
|||
|
|
NotificationCenter.default.addObserver(self, selector: #selector(TabTrayController.appWillResignActiveNotification), name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
|
|||
|
|
NotificationCenter.default.addObserver(self, selector: #selector(TabTrayController.appDidBecomeActiveNotification), name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil)
|
|||
|
|
NotificationCenter.default.addObserver(self, selector: #selector(TabTrayController.dynamicFontChanged(_:)), name: NotificationDynamicFontChanged, object: nil)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
override func viewWillAppear(_ animated: Bool) {
|
|||
|
|
super.viewWillAppear(animated)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
override func viewDidDisappear(_ animated: Bool) {
|
|||
|
|
super.viewDidDisappear(animated)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|||
|
|
super.traitCollectionDidChange(previousTraitCollection)
|
|||
|
|
|
|||
|
|
// Update the trait collection we reference in our layout delegate
|
|||
|
|
tabLayoutDelegate.traitCollection = traitCollection
|
|||
|
|
self.collectionView.collectionViewLayout.invalidateLayout()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fileprivate func cancelExistingGestures() {
|
|||
|
|
if let visibleCells = self.collectionView.visibleCells as? [TabCell] {
|
|||
|
|
for cell in visibleCells {
|
|||
|
|
cell.animator.cancelExistingGestures()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
|||
|
|
super.viewWillTransition(to: size, with: coordinator)
|
|||
|
|
coordinator.animate(alongsideTransition: { _ in
|
|||
|
|
self.collectionView.collectionViewLayout.invalidateLayout()
|
|||
|
|
}, completion: nil)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
|||
|
|
return UIStatusBarStyle.lightContent //this will need to be fixed
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fileprivate func makeConstraints() {
|
|||
|
|
collectionView.snp.makeConstraints { make in
|
|||
|
|
make.left.bottom.right.equalTo(view)
|
|||
|
|
make.top.equalTo(self.topLayoutGuide.snp.bottom)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
toolbar.snp.makeConstraints { make in
|
|||
|
|
make.left.right.bottom.equalTo(view)
|
|||
|
|
make.height.equalTo(UIConstants.BottomToolbarHeight)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MARK: Selectors
|
|||
|
|
func didClickDone() {
|
|||
|
|
presentingViewController!.dismiss(animated: true, completion: nil)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func didClickSettingsItem() {
|
|||
|
|
assert(Thread.isMainThread, "Opening settings requires being invoked on the main thread")
|
|||
|
|
|
|||
|
|
let settingsTableViewController = AppSettingsTableViewController()
|
|||
|
|
settingsTableViewController.profile = profile
|
|||
|
|
settingsTableViewController.tabManager = tabManager
|
|||
|
|
settingsTableViewController.settingsDelegate = self
|
|||
|
|
|
|||
|
|
let controller = SettingsNavigationController(rootViewController: settingsTableViewController)
|
|||
|
|
controller.popoverDelegate = self
|
|||
|
|
controller.modalPresentationStyle = UIModalPresentationStyle.formSheet
|
|||
|
|
present(controller, animated: true, completion: nil)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func didClickAddTab() {
|
|||
|
|
openNewTab()
|
|||
|
|
LeanPlumClient.shared.track(event: .openedNewTab, withParameters: ["Source": "Tab Tray" as AnyObject])
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func didTapLearnMore() {
|
|||
|
|
let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
|
|||
|
|
if let langID = Locale.preferredLanguages.first {
|
|||
|
|
let learnMoreRequest = URLRequest(url: "https://support.mozilla.org/1/mobile/\(appVersion)/iOS/\(langID)/private-browsing-ios".asURL!)
|
|||
|
|
openNewTab(learnMoreRequest)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func didTogglePrivateMode() {
|
|||
|
|
let scaleDownTransform = CGAffineTransform(scaleX: 0.9, y: 0.9)
|
|||
|
|
|
|||
|
|
let fromView: UIView
|
|||
|
|
if !privateTabsAreEmpty(), let snapshot = collectionView.snapshotView(afterScreenUpdates: false) {
|
|||
|
|
snapshot.frame = collectionView.frame
|
|||
|
|
view.insertSubview(snapshot, aboveSubview: collectionView)
|
|||
|
|
fromView = snapshot
|
|||
|
|
} else {
|
|||
|
|
fromView = emptyPrivateTabsView
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
tabManager.willSwitchTabMode()
|
|||
|
|
privateMode = !privateMode
|
|||
|
|
// If we are exiting private mode and we have the close private tabs option selected, make sure
|
|||
|
|
// we clear out all of the private tabs
|
|||
|
|
let exitingPrivateMode = !privateMode && tabManager.shouldClearPrivateTabs()
|
|||
|
|
|
|||
|
|
toolbar.maskButton.setSelected(privateMode, animated: true)
|
|||
|
|
collectionView.layoutSubviews()
|
|||
|
|
|
|||
|
|
let toView: UIView
|
|||
|
|
if !privateTabsAreEmpty(), let newSnapshot = collectionView.snapshotView(afterScreenUpdates: !exitingPrivateMode) {
|
|||
|
|
emptyPrivateTabsView.isHidden = true
|
|||
|
|
//when exiting private mode don't screenshot the collectionview (causes the UI to hang)
|
|||
|
|
newSnapshot.frame = collectionView.frame
|
|||
|
|
view.insertSubview(newSnapshot, aboveSubview: fromView)
|
|||
|
|
collectionView.alpha = 0
|
|||
|
|
toView = newSnapshot
|
|||
|
|
} else {
|
|||
|
|
emptyPrivateTabsView.isHidden = false
|
|||
|
|
toView = emptyPrivateTabsView
|
|||
|
|
}
|
|||
|
|
toView.alpha = 0
|
|||
|
|
toView.transform = scaleDownTransform
|
|||
|
|
|
|||
|
|
UIView.animate(withDuration: 0.2, delay: 0, options: [], animations: { () -> Void in
|
|||
|
|
fromView.transform = scaleDownTransform
|
|||
|
|
fromView.alpha = 0
|
|||
|
|
toView.transform = CGAffineTransform.identity
|
|||
|
|
toView.alpha = 1
|
|||
|
|
}) { finished in
|
|||
|
|
if fromView != self.emptyPrivateTabsView {
|
|||
|
|
fromView.removeFromSuperview()
|
|||
|
|
}
|
|||
|
|
if toView != self.emptyPrivateTabsView {
|
|||
|
|
toView.removeFromSuperview()
|
|||
|
|
}
|
|||
|
|
self.collectionView.alpha = 1
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fileprivate func privateTabsAreEmpty() -> Bool {
|
|||
|
|
return privateMode && tabManager.privateTabs.count == 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func changePrivacyMode(_ isPrivate: Bool) {
|
|||
|
|
if isPrivate != privateMode {
|
|||
|
|
guard let _ = collectionView else {
|
|||
|
|
privateMode = isPrivate
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
didTogglePrivateMode()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fileprivate func openNewTab(_ request: URLRequest? = nil) {
|
|||
|
|
toolbar.isUserInteractionEnabled = false
|
|||
|
|
|
|||
|
|
// We're only doing one update here, but using a batch update lets us delay selecting the tab
|
|||
|
|
// until after its insert animation finishes.
|
|||
|
|
self.collectionView.performBatchUpdates({ _ in
|
|||
|
|
_ = self.tabManager.addTab(request, isPrivate: self.privateMode)
|
|||
|
|
}, completion: { finished in
|
|||
|
|
// The addTab delegate method will pop to the BVC no need to do anything here.
|
|||
|
|
self.toolbar.isUserInteractionEnabled = true
|
|||
|
|
if finished {
|
|||
|
|
if request == nil && NewTabAccessors.getNewTabPage(self.profile.prefs) == .blankPage {
|
|||
|
|
if let bvc = self.navigationController?.topViewController as? BrowserViewController {
|
|||
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
|
|||
|
|
bvc.urlBar.tabLocationViewDidTapLocation(bvc.urlBar.locationView)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fileprivate func closeTabsForCurrentTray() {
|
|||
|
|
tabManager.removeTabsWithUndoToast(tabsToDisplay)
|
|||
|
|
self.collectionView.reloadData()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MARK: - App Notifications
|
|||
|
|
extension TabTrayController {
|
|||
|
|
func appWillResignActiveNotification() {
|
|||
|
|
if privateMode {
|
|||
|
|
collectionView.alpha = 0
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func appDidBecomeActiveNotification() {
|
|||
|
|
// Re-show any components that might have been hidden because they were being displayed
|
|||
|
|
// as part of a private mode tab
|
|||
|
|
UIView.animate(withDuration: 0.2, delay: 0, options: UIViewAnimationOptions(), animations: {
|
|||
|
|
self.collectionView.alpha = 1
|
|||
|
|
},
|
|||
|
|
completion: nil)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
extension TabTrayController: TabSelectionDelegate {
|
|||
|
|
func didSelectTabAtIndex(_ index: Int) {
|
|||
|
|
let tab = tabsToDisplay[index]
|
|||
|
|
tabManager.selectTab(tab)
|
|||
|
|
_ = self.navigationController?.popViewController(animated: true)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
extension TabTrayController: PresentingModalViewControllerDelegate {
|
|||
|
|
func dismissPresentedModalViewController(_ modalViewController: UIViewController, animated: Bool) {
|
|||
|
|
dismiss(animated: animated, completion: { self.collectionView.reloadData() })
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
extension TabTrayController: TabManagerDelegate {
|
|||
|
|
func tabManager(_ tabManager: TabManager, didSelectedTabChange selected: Tab?, previous: Tab?) {
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func tabManager(_ tabManager: TabManager, willAddTab tab: Tab) {
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func tabManager(_ tabManager: TabManager, willRemoveTab tab: Tab) {
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func tabManager(_ tabManager: TabManager, didAddTab tab: Tab) {
|
|||
|
|
// Get the index of the added tab from it's set (private or normal)
|
|||
|
|
guard let index = tabsToDisplay.index(of: tab) else { return }
|
|||
|
|
if !privateTabsAreEmpty() {
|
|||
|
|
emptyPrivateTabsView.isHidden = true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
tabDataSource.addTab(tab)
|
|||
|
|
self.collectionView?.performBatchUpdates({ _ in
|
|||
|
|
self.collectionView.insertItems(at: [IndexPath(item: index, section: 0)])
|
|||
|
|
}, completion: { finished in
|
|||
|
|
if finished {
|
|||
|
|
tabManager.selectTab(tab)
|
|||
|
|
// don't pop the tab tray view controller if it is not in the foreground
|
|||
|
|
if self.presentedViewController == nil {
|
|||
|
|
_ = self.navigationController?.popViewController(animated: true)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func tabManager(_ tabManager: TabManager, didRemoveTab tab: Tab) {
|
|||
|
|
// it is possible that we are removing a tab that we are not currently displaying
|
|||
|
|
// through the Close All Tabs feature (which will close tabs that are not in our current privacy mode)
|
|||
|
|
// check this before removing the item from the collection
|
|||
|
|
let removedIndex = tabDataSource.removeTab(tab)
|
|||
|
|
if removedIndex > -1 {
|
|||
|
|
self.collectionView.performBatchUpdates({
|
|||
|
|
self.collectionView.deleteItems(at: [IndexPath(item: removedIndex, section: 0)])
|
|||
|
|
}, completion: { finished in
|
|||
|
|
guard finished && self.privateTabsAreEmpty() else { return }
|
|||
|
|
self.emptyPrivateTabsView.isHidden = false
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func tabManagerDidAddTabs(_ tabManager: TabManager) {
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func tabManagerDidRestoreTabs(_ tabManager: TabManager) {
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func tabManagerDidRemoveAllTabs(_ tabManager: TabManager, toast: ButtonToast?) {
|
|||
|
|
guard privateMode else {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if let toast = toast {
|
|||
|
|
view.addSubview(toast)
|
|||
|
|
toast.snp.makeConstraints { make in
|
|||
|
|
make.left.right.equalTo(view)
|
|||
|
|
make.bottom.equalTo(toolbar.snp.top)
|
|||
|
|
}
|
|||
|
|
toast.showToast()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
extension TabTrayController: UIScrollViewAccessibilityDelegate {
|
|||
|
|
func accessibilityScrollStatus(for scrollView: UIScrollView) -> String? {
|
|||
|
|
var visibleCells = collectionView.visibleCells as! [TabCell]
|
|||
|
|
var bounds = collectionView.bounds
|
|||
|
|
bounds = bounds.offsetBy(dx: collectionView.contentInset.left, dy: collectionView.contentInset.top)
|
|||
|
|
bounds.size.width -= collectionView.contentInset.left + collectionView.contentInset.right
|
|||
|
|
bounds.size.height -= collectionView.contentInset.top + collectionView.contentInset.bottom
|
|||
|
|
// visible cells do sometimes return also not visible cells when attempting to go past the last cell with VoiceOver right-flick gesture; so make sure we have only visible cells (yeah...)
|
|||
|
|
visibleCells = visibleCells.filter { !$0.frame.intersection(bounds).isEmpty }
|
|||
|
|
|
|||
|
|
let cells = visibleCells.map { self.collectionView.indexPath(for: $0)! }
|
|||
|
|
let indexPaths = cells.sorted { (a: IndexPath, b: IndexPath) -> Bool in
|
|||
|
|
return a.section < b.section || (a.section == b.section && a.row < b.row)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if indexPaths.count == 0 {
|
|||
|
|
return NSLocalizedString("No tabs", comment: "Message spoken by VoiceOver to indicate that there are no tabs in the Tabs Tray")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let firstTab = indexPaths.first!.row + 1
|
|||
|
|
let lastTab = indexPaths.last!.row + 1
|
|||
|
|
let tabCount = collectionView.numberOfItems(inSection: 0)
|
|||
|
|
|
|||
|
|
if firstTab == lastTab {
|
|||
|
|
let format = NSLocalizedString("Tab %@ of %@", comment: "Message spoken by VoiceOver saying the position of the single currently visible tab in Tabs Tray, along with the total number of tabs. E.g. \"Tab 2 of 5\" says that tab 2 is visible (and is the only visible tab), out of 5 tabs total.")
|
|||
|
|
return String(format: format, NSNumber(value: firstTab as Int), NSNumber(value: tabCount as Int))
|
|||
|
|
} else {
|
|||
|
|
let format = NSLocalizedString("Tabs %@ to %@ of %@", comment: "Message spoken by VoiceOver saying the range of tabs that are currently visible in Tabs Tray, along with the total number of tabs. E.g. \"Tabs 8 to 10 of 15\" says tabs 8, 9 and 10 are visible, out of 15 tabs total.")
|
|||
|
|
return String(format: format, NSNumber(value: firstTab as Int), NSNumber(value: lastTab as Int), NSNumber(value: tabCount as Int))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
extension TabTrayController: SwipeAnimatorDelegate {
|
|||
|
|
func swipeAnimator(_ animator: SwipeAnimator, viewWillExitContainerBounds: UIView) {
|
|||
|
|
let tabCell = animator.animatingView as! TabCell
|
|||
|
|
if let indexPath = collectionView.indexPath(for: tabCell) {
|
|||
|
|
let tab = tabsToDisplay[indexPath.item]
|
|||
|
|
tabManager.removeTab(tab)
|
|||
|
|
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, NSLocalizedString("Closing tab", comment: "Accessibility label (used by assistive technology) notifying the user that the tab is being closed."))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
extension TabTrayController: TabCellDelegate {
|
|||
|
|
func tabCellDidClose(_ cell: TabCell) {
|
|||
|
|
let indexPath = collectionView.indexPath(for: cell)!
|
|||
|
|
let tab = tabsToDisplay[indexPath.item]
|
|||
|
|
tabManager.removeTab(tab)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
extension TabTrayController: SettingsDelegate {
|
|||
|
|
func settingsOpenURLInNewTab(_ url: URL) {
|
|||
|
|
let request = URLRequest(url: url)
|
|||
|
|
openNewTab(request)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
extension TabTrayController: PhotonActionSheetProtocol {
|
|||
|
|
func didTapDelete(_ sender: UIButton) {
|
|||
|
|
let controller = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
|||
|
|
controller.addAction(UIAlertAction(title: Strings.AppMenuCloseAllTabsTitleString, style: .default, handler: { _ in self.closeTabsForCurrentTray() }))
|
|||
|
|
controller.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "Label for Cancel button"), style: .cancel, handler: nil))
|
|||
|
|
controller.popoverPresentationController?.sourceView = sender
|
|||
|
|
controller.popoverPresentationController?.sourceRect = sender.bounds
|
|||
|
|
present(controller, animated: true, completion: nil)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fileprivate class TabManagerDataSource: NSObject, UICollectionViewDataSource {
|
|||
|
|
unowned var cellDelegate: TabCellDelegate & SwipeAnimatorDelegate
|
|||
|
|
fileprivate var tabs: [Tab]
|
|||
|
|
fileprivate var tabManager: TabManager
|
|||
|
|
|
|||
|
|
init(tabs: [Tab], cellDelegate: TabCellDelegate & SwipeAnimatorDelegate, tabManager: TabManager) {
|
|||
|
|
self.cellDelegate = cellDelegate
|
|||
|
|
self.tabs = tabs
|
|||
|
|
self.tabManager = tabManager
|
|||
|
|
super.init()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
Removes the given tab from the data source
|
|||
|
|
|
|||
|
|
- parameter tab: Tab to remove
|
|||
|
|
|
|||
|
|
- returns: The index of the removed tab, -1 if tab did not exist
|
|||
|
|
*/
|
|||
|
|
func removeTab(_ tabToRemove: Tab) -> Int {
|
|||
|
|
var index: Int = -1
|
|||
|
|
for (i, tab) in tabs.enumerated() where tabToRemove === tab {
|
|||
|
|
index = i
|
|||
|
|
tabs.remove(at: index)
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
return index
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
Adds the given tab to the data source
|
|||
|
|
|
|||
|
|
- parameter tab: Tab to add
|
|||
|
|
*/
|
|||
|
|
func addTab(_ tab: Tab) {
|
|||
|
|
tabs.append(tab)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@objc func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
|||
|
|
let tabCell = collectionView.dequeueReusableCell(withReuseIdentifier: TabCell.Identifier, for: indexPath) as! TabCell
|
|||
|
|
tabCell.animator.delegate = cellDelegate
|
|||
|
|
tabCell.delegate = cellDelegate
|
|||
|
|
|
|||
|
|
let tab = tabs[indexPath.item]
|
|||
|
|
tabCell.style = tab.isPrivate ? .dark : .light
|
|||
|
|
tabCell.titleText.text = tab.displayTitle
|
|||
|
|
tabCell.closeButton.tintColor = tab.isPrivate ? UIColor.white : UIColor.gray
|
|||
|
|
|
|||
|
|
if !tab.displayTitle.isEmpty {
|
|||
|
|
tabCell.accessibilityLabel = tab.displayTitle
|
|||
|
|
} else {
|
|||
|
|
tabCell.accessibilityLabel = tab.url?.aboutComponent ?? "" // If there is no title we are most likely on a home panel.
|
|||
|
|
}
|
|||
|
|
tabCell.isAccessibilityElement = true
|
|||
|
|
tabCell.accessibilityHint = NSLocalizedString("Swipe right or left with three fingers to close the tab.", comment: "Accessibility hint for tab tray's displayed tab.")
|
|||
|
|
|
|||
|
|
if let favIcon = tab.displayFavicon {
|
|||
|
|
tabCell.favicon.sd_setImage(with: URL(string: favIcon.url)!)
|
|||
|
|
} else {
|
|||
|
|
let defaultFavicon = UIImage(named: "defaultFavicon")
|
|||
|
|
if tab.isPrivate {
|
|||
|
|
tabCell.favicon.image = defaultFavicon
|
|||
|
|
tabCell.favicon.tintColor = UIColor.white
|
|||
|
|
} else {
|
|||
|
|
tabCell.favicon.image = defaultFavicon
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if tab == tabManager.selectedTab {
|
|||
|
|
tabCell.setTabSelected(tab.isPrivate)
|
|||
|
|
}
|
|||
|
|
tabCell.screenshotView.image = tab.screenshot
|
|||
|
|
return tabCell
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@objc func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
|||
|
|
return tabs.count
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@objc fileprivate func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
|
|||
|
|
let fromIndex = sourceIndexPath.item
|
|||
|
|
let toIndex = destinationIndexPath.item
|
|||
|
|
tabs.insert(tabs.remove(at: fromIndex), at: toIndex < fromIndex ? toIndex : toIndex - 1)
|
|||
|
|
tabManager.moveTab(isPrivate: tabs[fromIndex].isPrivate, fromIndex: fromIndex, toIndex: toIndex)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@objc protocol TabSelectionDelegate: class {
|
|||
|
|
func didSelectTabAtIndex(_ index: Int)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fileprivate class TabLayoutDelegate: NSObject, UICollectionViewDelegateFlowLayout {
|
|||
|
|
weak var tabSelectionDelegate: TabSelectionDelegate?
|
|||
|
|
|
|||
|
|
fileprivate var traitCollection: UITraitCollection
|
|||
|
|
fileprivate var profile: Profile
|
|||
|
|
fileprivate var numberOfColumns: Int {
|
|||
|
|
// iPhone 4-6+ portrait
|
|||
|
|
if traitCollection.horizontalSizeClass == .compact && traitCollection.verticalSizeClass == .regular {
|
|||
|
|
return TabTrayControllerUX.CompactNumberOfColumnsThin
|
|||
|
|
} else {
|
|||
|
|
return TabTrayControllerUX.NumberOfColumnsWide
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
init(profile: Profile, traitCollection: UITraitCollection) {
|
|||
|
|
self.profile = profile
|
|||
|
|
self.traitCollection = traitCollection
|
|||
|
|
super.init()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fileprivate func cellHeightForCurrentDevice() -> CGFloat {
|
|||
|
|
let shortHeight = TabTrayControllerUX.TextBoxHeight * 6
|
|||
|
|
|
|||
|
|
if self.traitCollection.verticalSizeClass == UIUserInterfaceSizeClass.compact {
|
|||
|
|
return shortHeight
|
|||
|
|
} else if self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClass.compact {
|
|||
|
|
return shortHeight
|
|||
|
|
} else {
|
|||
|
|
return TabTrayControllerUX.TextBoxHeight * 8
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@objc func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
|
|||
|
|
return TabTrayControllerUX.Margin
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@objc func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
|||
|
|
let cellWidth = floor((collectionView.bounds.width - TabTrayControllerUX.Margin * CGFloat(numberOfColumns + 1)) / CGFloat(numberOfColumns))
|
|||
|
|
return CGSize(width: cellWidth, height: self.cellHeightForCurrentDevice())
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@objc func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
|
|||
|
|
return UIEdgeInsets(equalInset: TabTrayControllerUX.Margin)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@objc func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
|
|||
|
|
return TabTrayControllerUX.Margin
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@objc func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
|||
|
|
tabSelectionDelegate?.didSelectTabAtIndex(indexPath.row)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
struct EmptyPrivateTabsViewUX {
|
|||
|
|
static let TitleColor = UIColor.white
|
|||
|
|
static let TitleFont = UIFont.systemFont(ofSize: 22, weight: UIFontWeightMedium)
|
|||
|
|
static let DescriptionColor = UIColor.white
|
|||
|
|
static let DescriptionFont = UIFont.systemFont(ofSize: 17)
|
|||
|
|
static let LearnMoreFont = UIFont.systemFont(ofSize: 15, weight: UIFontWeightMedium)
|
|||
|
|
static let TextMargin: CGFloat = 18
|
|||
|
|
static let LearnMoreMargin: CGFloat = 30
|
|||
|
|
static let MaxDescriptionWidth: CGFloat = 250
|
|||
|
|
static let MinBottomMargin: CGFloat = 10
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// View we display when there are no private tabs created
|
|||
|
|
fileprivate class EmptyPrivateTabsView: UIView {
|
|||
|
|
fileprivate lazy var titleLabel: UILabel = {
|
|||
|
|
let label = UILabel()
|
|||
|
|
label.textColor = EmptyPrivateTabsViewUX.TitleColor
|
|||
|
|
label.font = EmptyPrivateTabsViewUX.TitleFont
|
|||
|
|
label.textAlignment = NSTextAlignment.center
|
|||
|
|
return label
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
fileprivate var descriptionLabel: UILabel = {
|
|||
|
|
let label = UILabel()
|
|||
|
|
label.textColor = EmptyPrivateTabsViewUX.DescriptionColor
|
|||
|
|
label.font = EmptyPrivateTabsViewUX.DescriptionFont
|
|||
|
|
label.textAlignment = NSTextAlignment.center
|
|||
|
|
label.numberOfLines = 0
|
|||
|
|
label.preferredMaxLayoutWidth = EmptyPrivateTabsViewUX.MaxDescriptionWidth
|
|||
|
|
return label
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
fileprivate var learnMoreButton: UIButton = {
|
|||
|
|
let button = UIButton(type: .system)
|
|||
|
|
button.setTitle(
|
|||
|
|
NSLocalizedString("Learn More", tableName: "PrivateBrowsing", comment: "Text button displayed when there are no tabs open while in private mode"),
|
|||
|
|
for: UIControlState())
|
|||
|
|
button.setTitleColor(UIConstants.PrivateModeTextHighlightColor, for: UIControlState())
|
|||
|
|
button.titleLabel?.font = EmptyPrivateTabsViewUX.LearnMoreFont
|
|||
|
|
return button
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
fileprivate var iconImageView: UIImageView = {
|
|||
|
|
let imageView = UIImageView(image: UIImage(named: "largePrivateMask"))
|
|||
|
|
return imageView
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
override init(frame: CGRect) {
|
|||
|
|
super.init(frame: frame)
|
|||
|
|
|
|||
|
|
titleLabel.text = NSLocalizedString("Private Browsing",
|
|||
|
|
tableName: "PrivateBrowsing", comment: "Title displayed for when there are no open tabs while in private mode")
|
|||
|
|
descriptionLabel.text = NSLocalizedString("Firefox won’t remember any of your history or cookies, but new bookmarks will be saved.",
|
|||
|
|
tableName: "PrivateBrowsing", comment: "Description text displayed when there are no open tabs while in private mode")
|
|||
|
|
|
|||
|
|
addSubview(titleLabel)
|
|||
|
|
addSubview(descriptionLabel)
|
|||
|
|
addSubview(iconImageView)
|
|||
|
|
addSubview(learnMoreButton)
|
|||
|
|
|
|||
|
|
titleLabel.snp.makeConstraints { make in
|
|||
|
|
make.center.equalTo(self)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
iconImageView.snp.makeConstraints { make in
|
|||
|
|
make.bottom.equalTo(titleLabel.snp.top).offset(-EmptyPrivateTabsViewUX.TextMargin)
|
|||
|
|
make.centerX.equalTo(self)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
descriptionLabel.snp.makeConstraints { make in
|
|||
|
|
make.top.equalTo(titleLabel.snp.bottom).offset(EmptyPrivateTabsViewUX.TextMargin)
|
|||
|
|
make.centerX.equalTo(self)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
learnMoreButton.snp.makeConstraints { (make) -> Void in
|
|||
|
|
make.top.equalTo(descriptionLabel.snp.bottom).offset(EmptyPrivateTabsViewUX.LearnMoreMargin).priority(10)
|
|||
|
|
make.bottom.lessThanOrEqualTo(self).offset(-EmptyPrivateTabsViewUX.MinBottomMargin).priority(1000)
|
|||
|
|
make.centerX.equalTo(self)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
required init?(coder aDecoder: NSCoder) {
|
|||
|
|
fatalError("init(coder:) has not been implemented")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
extension TabTrayController: TabPeekDelegate {
|
|||
|
|
|
|||
|
|
func tabPeekDidAddBookmark(_ tab: Tab) {
|
|||
|
|
delegate?.tabTrayDidAddBookmark(tab)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func tabPeekDidAddToReadingList(_ tab: Tab) -> ReadingListClientRecord? {
|
|||
|
|
return delegate?.tabTrayDidAddToReadingList(tab)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func tabPeekDidCloseTab(_ tab: Tab) {
|
|||
|
|
if let index = self.tabDataSource.tabs.index(of: tab),
|
|||
|
|
let cell = self.collectionView?.cellForItem(at: IndexPath(item: index, section: 0)) as? TabCell {
|
|||
|
|
cell.close()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func tabPeekRequestsPresentationOf(_ viewController: UIViewController) {
|
|||
|
|
delegate?.tabTrayRequestsPresentationOf(viewController)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
extension TabTrayController: UIViewControllerPreviewingDelegate {
|
|||
|
|
|
|||
|
|
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {
|
|||
|
|
|
|||
|
|
guard let collectionView = collectionView else { return nil }
|
|||
|
|
let convertedLocation = self.view.convert(location, to: collectionView)
|
|||
|
|
|
|||
|
|
guard let indexPath = collectionView.indexPathForItem(at: convertedLocation),
|
|||
|
|
let cell = collectionView.cellForItem(at: indexPath) else { return nil }
|
|||
|
|
|
|||
|
|
let tab = tabDataSource.tabs[indexPath.row]
|
|||
|
|
let tabVC = TabPeekViewController(tab: tab, delegate: self)
|
|||
|
|
if let browserProfile = profile as? BrowserProfile {
|
|||
|
|
tabVC.setState(withProfile: browserProfile, clientPickerDelegate: self)
|
|||
|
|
}
|
|||
|
|
previewingContext.sourceRect = self.view.convert(cell.frame, from: collectionView)
|
|||
|
|
|
|||
|
|
return tabVC
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) {
|
|||
|
|
guard let tpvc = viewControllerToCommit as? TabPeekViewController else { return }
|
|||
|
|
tabManager.selectTab(tpvc.tab)
|
|||
|
|
navigationController?.popViewController(animated: true)
|
|||
|
|
delegate?.tabTrayDidDismiss(self)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
extension TabTrayController: ClientPickerViewControllerDelegate {
|
|||
|
|
|
|||
|
|
func clientPickerViewController(_ clientPickerViewController: ClientPickerViewController, didPickClients clients: [RemoteClient]) {
|
|||
|
|
if let item = clientPickerViewController.shareItem {
|
|||
|
|
_ = self.profile.sendItems([item], toClients: clients)
|
|||
|
|
}
|
|||
|
|
clientPickerViewController.dismiss(animated: true, completion: nil)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func clientPickerViewControllerDidCancel(_ clientPickerViewController: ClientPickerViewController) {
|
|||
|
|
clientPickerViewController.dismiss(animated: true, completion: nil)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
extension TabTrayController: UIAdaptivePresentationControllerDelegate, UIPopoverPresentationControllerDelegate {
|
|||
|
|
// Returning None here makes sure that the Popover is actually presented as a Popover and
|
|||
|
|
// not as a full-screen modal, which is the default on compact device classes.
|
|||
|
|
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
|
|||
|
|
return UIModalPresentationStyle.none
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MARK: - Toolbar
|
|||
|
|
class TrayToolbar: UIView {
|
|||
|
|
fileprivate let toolbarButtonSize = CGSize(width: 44, height: 44)
|
|||
|
|
|
|||
|
|
lazy var addTabButton: UIButton = {
|
|||
|
|
let button = UIButton()
|
|||
|
|
button.setImage(UIImage.templateImageNamed("nav-add"), for: .normal)
|
|||
|
|
button.accessibilityLabel = NSLocalizedString("Add Tab", comment: "Accessibility label for the Add Tab button in the Tab Tray.")
|
|||
|
|
button.accessibilityIdentifier = "TabTrayController.addTabButton"
|
|||
|
|
return button
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
lazy var deleteButton: UIButton = {
|
|||
|
|
let button = UIButton()
|
|||
|
|
button.setImage(UIImage.templateImageNamed("action_delete"), for: .normal)
|
|||
|
|
button.accessibilityLabel = Strings.TabTrayDeleteMenuButtonAccessibilityLabel
|
|||
|
|
button.accessibilityIdentifier = "TabTrayController.removeTabsButton"
|
|||
|
|
return button
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
lazy var maskButton: PrivateModeButton = PrivateModeButton()
|
|||
|
|
fileprivate let sideOffset: CGFloat = 32
|
|||
|
|
|
|||
|
|
fileprivate override init(frame: CGRect) {
|
|||
|
|
super.init(frame: frame)
|
|||
|
|
backgroundColor = .white
|
|||
|
|
addSubview(addTabButton)
|
|||
|
|
|
|||
|
|
var buttonToCenter: UIButton?
|
|||
|
|
addSubview(deleteButton)
|
|||
|
|
buttonToCenter = deleteButton
|
|||
|
|
|
|||
|
|
maskButton.accessibilityIdentifier = "TabTrayController.maskButton"
|
|||
|
|
|
|||
|
|
buttonToCenter?.snp.makeConstraints { make in
|
|||
|
|
make.centerX.equalTo(self)
|
|||
|
|
make.top.equalTo(self)
|
|||
|
|
make.size.equalTo(toolbarButtonSize)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
addTabButton.snp.makeConstraints { make in
|
|||
|
|
make.top.equalTo(self)
|
|||
|
|
make.right.equalTo(self).offset(-sideOffset)
|
|||
|
|
make.size.equalTo(toolbarButtonSize)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
addSubview(maskButton)
|
|||
|
|
maskButton.snp.makeConstraints { make in
|
|||
|
|
make.top.equalTo(self)
|
|||
|
|
make.left.equalTo(self).offset(sideOffset)
|
|||
|
|
make.size.equalTo(toolbarButtonSize)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
styleToolbar(false)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
required init?(coder aDecoder: NSCoder) {
|
|||
|
|
fatalError("init(coder:) has not been implemented")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fileprivate func styleToolbar(_ isPrivate: Bool) {
|
|||
|
|
addTabButton.tintColor = isPrivate ? UIColor(rgb: 0xf9f9fa) : UIColor(rgb: 0x272727)
|
|||
|
|
deleteButton.tintColor = isPrivate ? UIColor(rgb: 0xf9f9fa) : UIColor(rgb: 0x272727)
|
|||
|
|
backgroundColor = isPrivate ? UIConstants.PrivateModeToolbarTintColor : UIColor(rgb: 0xf9f9fa)
|
|||
|
|
maskButton.styleForMode(privateMode: isPrivate)
|
|||
|
|
}
|
|||
|
|
}
|