Dactyloidae/mobile/ios/Client/Frontend/Browser/URLBarView.swift
2026-06-26 21:04:09 -07:00

825 lines
32 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 Shared
import SnapKit
import XCGLogger
private let log = Logger.browserLogger
struct URLBarViewUX {
static let TextFieldBorderColor = UIColor(rgb: 0xBBBBBB)
static let TextFieldActiveBorderColor = UIColor(rgb: 0xB0D5FB)
static let LocationLeftPadding: CGFloat = 8
static let Padding: CGFloat = 10
static let LocationHeight: CGFloat = 40
static let ButtonHeight: CGFloat = 44
static let LocationContentOffset: CGFloat = 8
static let TextFieldCornerRadius: CGFloat = 8
static let TextFieldBorderWidth: CGFloat = 1
static let TextFieldBorderWidthSelected: CGFloat = 4
// offset from edge of tabs button
static let ProgressTintColor = UIColor(rgb: 0x00dcfc)
static let ProgressBarHeight: CGFloat = 3
static let TabsButtonRotationOffset: CGFloat = 1.5
static let TabsButtonHeight: CGFloat = 18.0
static let ToolbarButtonInsets = UIEdgeInsets(top: Padding, left: Padding, bottom: Padding, right: Padding)
static let Themes: [String: Theme] = {
var themes = [String: Theme]()
var theme = Theme()
theme.borderColor = UIColor(rgb: 0x2D2D31)
theme.backgroundColor = UIColor(rgb: 0x38383D)
theme.activeBorderColor = UIColor(rgb: 0x4a4a4f)
theme.tintColor = UIColor(rgb: 0xf9f9fa)
theme.textColor = UIColor(rgb: 0xf9f9fa)
theme.buttonTintColor = UIColor(rgb: 0xD2d2d4)
theme.disabledButtonColor = UIColor.gray
theme.highlightButtonColor = UIColor(rgb: 0xAC39FF)
themes[Theme.PrivateMode] = theme
theme = Theme()
theme.borderColor = UIColor(rgb: 0x737373).withAlphaComponent(0.3)
theme.activeBorderColor = TextFieldActiveBorderColor
theme.disabledButtonColor = UIColor.lightGray
theme.highlightButtonColor = UIColor(rgb: 0x00A2FE)
theme.tintColor = ProgressTintColor
theme.textColor = UIColor(rgb: 0x272727)
theme.backgroundColor = UIConstants.AppBackgroundColor
theme.buttonTintColor = UIColor(rgb: 0x272727)
themes[Theme.NormalMode] = theme
return themes
}()
}
protocol URLBarDelegate: class {
func urlBarDidPressTabs(_ urlBar: URLBarView)
func urlBarDidPressReaderMode(_ urlBar: URLBarView)
/// - returns: whether the long-press was handled by the delegate; i.e. return `false` when the conditions for even starting handling long-press were not satisfied
func urlBarDidLongPressReaderMode(_ urlBar: URLBarView) -> Bool
func urlBarDidPressStop(_ urlBar: URLBarView)
func urlBarDidPressReload(_ urlBar: URLBarView)
func urlBarDidEnterOverlayMode(_ urlBar: URLBarView)
func urlBarDidLeaveOverlayMode(_ urlBar: URLBarView)
func urlBarDidLongPressLocation(_ urlBar: URLBarView)
func urlBarDidPressQRButton(_ urlBar: URLBarView)
func urlBarDidPressPageOptions(_ urlBar: URLBarView, from button: UIButton)
func urlBarLocationAccessibilityActions(_ urlBar: URLBarView) -> [UIAccessibilityCustomAction]?
func urlBarDidPressScrollToTop(_ urlBar: URLBarView)
func urlBar(_ urlBar: URLBarView, didEnterText text: String)
func urlBar(_ urlBar: URLBarView, didSubmitText text: String)
// Returns either (search query, true) or (url, false).
func urlBarDisplayTextForURL(_ url: URL?) -> (String?, Bool)
func urlBarDidLongPressPageOptions(_ urlBar: URLBarView, from button: UIButton)
}
class URLBarView: UIView {
// Additional UIAppearance-configurable properties
dynamic var locationBorderColor: UIColor = URLBarViewUX.TextFieldBorderColor {
didSet {
if !inOverlayMode {
locationContainer.layer.borderColor = locationBorderColor.cgColor
}
}
}
dynamic var locationActiveBorderColor: UIColor = URLBarViewUX.TextFieldActiveBorderColor {
didSet {
if inOverlayMode {
locationContainer.layer.borderColor = locationActiveBorderColor.cgColor
}
}
}
weak var delegate: URLBarDelegate?
weak var tabToolbarDelegate: TabToolbarDelegate?
var helper: TabToolbarHelper?
var isTransitioning: Bool = false {
didSet {
if isTransitioning {
// Cancel any pending/in-progress animations related to the progress bar
self.progressBar.setProgress(1, animated: false)
self.progressBar.alpha = 0.0
}
}
}
fileprivate var currentTheme: String = Theme.NormalMode
var toolbarIsShowing = false
var topTabsIsShowing = false
fileprivate var locationTextField: ToolbarTextField?
/// Overlay mode is the state where the lock/reader icons are hidden, the home panels are shown,
/// and the Cancel button is visible (allowing the user to leave overlay mode). Overlay mode
/// is *not* tied to the location text field's editing state; for instance, when selecting
/// a panel, the first responder will be resigned, yet the overlay mode UI is still active.
var inOverlayMode = false
lazy var locationView: TabLocationView = {
let locationView = TabLocationView()
locationView.translatesAutoresizingMaskIntoConstraints = false
locationView.readerModeState = ReaderModeState.unavailable
locationView.delegate = self
return locationView
}()
lazy var locationContainer: UIView = {
let locationContainer = TabLocationContainerView()
locationContainer.translatesAutoresizingMaskIntoConstraints = false
locationContainer.layer.shadowColor = self.locationBorderColor.cgColor
locationContainer.layer.borderWidth = URLBarViewUX.TextFieldBorderWidth
locationContainer.layer.borderColor = self.locationBorderColor.cgColor
locationContainer.backgroundColor = .clear
return locationContainer
}()
let line = UIView()
lazy var tabsButton: TabsButton = {
let tabsButton = TabsButton.tabTrayButton()
tabsButton.accessibilityIdentifier = "URLBarView.tabsButton"
return tabsButton
}()
fileprivate lazy var progressBar: GradientProgressBar = {
let progressBar = GradientProgressBar()
progressBar.clipsToBounds = false
return progressBar
}()
fileprivate lazy var cancelButton: UIButton = {
let cancelButton = InsetButton()
cancelButton.setImage(UIImage.templateImageNamed("goBack"), for: .normal)
cancelButton.addTarget(self, action: #selector(URLBarView.SELdidClickCancel), for: .touchUpInside)
cancelButton.alpha = 0
return cancelButton
}()
fileprivate lazy var showQRScannerButton: InsetButton = {
let button = InsetButton()
button.setImage(UIImage.templateImageNamed("menu-ScanQRCode"), for: .normal)
button.clipsToBounds = false
button.addTarget(self, action: #selector(URLBarView.showQRScanner), for: .touchUpInside)
button.setContentHuggingPriority(1000, for: UILayoutConstraintAxis.horizontal)
button.setContentCompressionResistancePriority(1000, for: UILayoutConstraintAxis.horizontal)
return button
}()
fileprivate lazy var scrollToTopButton: UIButton = {
let button = UIButton()
button.addTarget(self, action: #selector(URLBarView.SELtappedScrollToTopArea), for: .touchUpInside)
return button
}()
var menuButton = ToolbarButton()
var bookmarkButton = ToolbarButton()
var forwardButton = ToolbarButton()
var stopReloadButton = ToolbarButton()
var backButton: ToolbarButton = {
let backButton = ToolbarButton()
backButton.accessibilityIdentifier = "URLBarView.backButton"
return backButton
}()
lazy var actionButtons: [Themeable & UIButton] = [self.tabsButton, self.menuButton, self.forwardButton, self.backButton, self.stopReloadButton]
var currentURL: URL? {
get {
return locationView.url as URL?
}
set(newURL) {
locationView.url = newURL
line.isHidden = newURL?.isAboutHomeURL ?? true
}
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
fileprivate func commonInit() {
locationContainer.addSubview(locationView)
[scrollToTopButton, line, tabsButton, progressBar, cancelButton, showQRScannerButton].forEach { addSubview($0) }
[menuButton, forwardButton, backButton, stopReloadButton, locationContainer].forEach { addSubview($0) }
helper = TabToolbarHelper(toolbar: self)
setupConstraints()
// Make sure we hide any views that shouldn't be showing in non-overlay mode.
updateViewsForOverlayModeAndToolbarChanges()
}
fileprivate func setupConstraints() {
line.snp.makeConstraints { make in
make.bottom.leading.trailing.equalTo(self)
make.height.equalTo(1)
}
scrollToTopButton.snp.makeConstraints { make in
make.top.equalTo(self)
make.left.right.equalTo(self.locationContainer)
}
progressBar.snp.makeConstraints { make in
make.top.equalTo(self.snp.bottom).inset(URLBarViewUX.ProgressBarHeight / 2)
make.height.equalTo(URLBarViewUX.ProgressBarHeight)
make.left.right.equalTo(self)
}
locationView.snp.makeConstraints { make in
make.edges.equalTo(self.locationContainer)
}
cancelButton.snp.makeConstraints { make in
make.centerY.equalTo(self.locationContainer)
make.size.equalTo(URLBarViewUX.ButtonHeight)
make.leading.equalTo(self)
}
backButton.snp.makeConstraints { make in
make.centerY.equalTo(self)
make.leading.equalTo(self).offset(URLBarViewUX.Padding)
make.size.equalTo(URLBarViewUX.ButtonHeight)
}
forwardButton.snp.makeConstraints { make in
make.left.equalTo(self.backButton.snp.right)
make.centerY.equalTo(self)
make.size.equalTo(URLBarViewUX.ButtonHeight)
}
stopReloadButton.snp.makeConstraints { make in
make.left.equalTo(self.forwardButton.snp.right)
make.centerY.equalTo(self)
make.size.equalTo(URLBarViewUX.ButtonHeight)
}
menuButton.snp.makeConstraints { make in
make.trailing.equalTo(self.snp.trailing).offset(-URLBarViewUX.Padding)
make.centerY.equalTo(self)
make.size.equalTo(URLBarViewUX.ButtonHeight)
}
tabsButton.snp.makeConstraints { make in
make.trailing.equalTo(self.menuButton.snp.leading)
make.centerY.equalTo(self)
make.size.equalTo(URLBarViewUX.ButtonHeight)
}
showQRScannerButton.snp.makeConstraints { make in
make.centerY.equalTo(self.locationContainer)
make.trailing.equalTo(self)
make.size.equalTo(URLBarViewUX.ButtonHeight)
}
}
override func updateConstraints() {
super.updateConstraints()
if inOverlayMode {
// In overlay mode, we always show the location view full width
self.locationContainer.layer.borderWidth = URLBarViewUX.TextFieldBorderWidthSelected
self.locationContainer.snp.remakeConstraints { make in
let height = URLBarViewUX.LocationHeight + (URLBarViewUX.TextFieldBorderWidthSelected * 2)
make.height.equalTo(height)
make.trailing.equalTo(self.showQRScannerButton.snp.leading)
make.leading.equalTo(self.cancelButton.snp.trailing)
make.centerY.equalTo(self)
}
self.locationView.snp.remakeConstraints { make in
make.edges.equalTo(self.locationContainer).inset(UIEdgeInsets(equalInset: URLBarViewUX.TextFieldBorderWidthSelected))
}
self.locationTextField?.snp.remakeConstraints { make in
make.edges.equalTo(self.locationView).inset(UIEdgeInsets(top: 0, left: URLBarViewUX.LocationLeftPadding, bottom: 0, right: URLBarViewUX.LocationLeftPadding))
}
} else {
self.locationContainer.snp.remakeConstraints { make in
if self.toolbarIsShowing {
// If we are showing a toolbar, show the text field next to the forward button
make.leading.equalTo(self.stopReloadButton.snp.trailing).offset(URLBarViewUX.Padding)
if self.topTabsIsShowing {
make.trailing.equalTo(self.menuButton.snp.leading).offset(-URLBarViewUX.Padding)
} else {
make.trailing.equalTo(self.tabsButton.snp.leading).offset(-URLBarViewUX.Padding)
}
} else {
// Otherwise, left align the location view
make.leading.trailing.equalTo(self).inset(UIEdgeInsets(top: 0, left: URLBarViewUX.LocationLeftPadding-1, bottom: 0, right: URLBarViewUX.LocationLeftPadding-1))
}
make.height.equalTo(URLBarViewUX.LocationHeight+2)
make.centerY.equalTo(self)
}
self.locationContainer.layer.borderWidth = URLBarViewUX.TextFieldBorderWidth
self.locationView.snp.remakeConstraints { make in
make.edges.equalTo(self.locationContainer).inset(UIEdgeInsets(equalInset: URLBarViewUX.TextFieldBorderWidth))
}
}
}
func showQRScanner() {
self.delegate?.urlBarDidPressQRButton(self)
}
func createLocationTextField() {
guard locationTextField == nil else { return }
locationTextField = ToolbarTextField()
guard let locationTextField = locationTextField else { return }
locationTextField.translatesAutoresizingMaskIntoConstraints = false
locationTextField.autocompleteDelegate = self
locationTextField.keyboardType = UIKeyboardType.webSearch
locationTextField.autocorrectionType = UITextAutocorrectionType.no
locationTextField.autocapitalizationType = UITextAutocapitalizationType.none
locationTextField.returnKeyType = UIReturnKeyType.go
locationTextField.clearButtonMode = UITextFieldViewMode.whileEditing
locationTextField.font = UIConstants.DefaultChromeFont
locationTextField.accessibilityIdentifier = "address"
locationTextField.accessibilityLabel = NSLocalizedString("Address and Search", comment: "Accessibility label for address and search field, both words (Address, Search) are therefore nouns.")
locationTextField.attributedPlaceholder = self.locationView.placeholder
locationContainer.addSubview(locationTextField)
locationTextField.snp.remakeConstraints { make in
make.edges.equalTo(self.locationView)
}
locationTextField.applyTheme(currentTheme)
}
func removeLocationTextField() {
locationTextField?.removeFromSuperview()
locationTextField = nil
}
// Ideally we'd split this implementation in two, one URLBarView with a toolbar and one without
// However, switching views dynamically at runtime is a difficult. For now, we just use one view
// that can show in either mode.
func setShowToolbar(_ shouldShow: Bool) {
toolbarIsShowing = shouldShow
setNeedsUpdateConstraints()
// when we transition from portrait to landscape, calling this here causes
// the constraints to be calculated too early and there are constraint errors
if !toolbarIsShowing {
updateConstraintsIfNeeded()
}
updateViewsForOverlayModeAndToolbarChanges()
}
func updateAlphaForSubviews(_ alpha: CGFloat) {
self.locationContainer.alpha = alpha
self.alpha = alpha
}
func updateProgressBar(_ progress: Float) {
progressBar.alpha = 1
progressBar.isHidden = false
progressBar.setProgress(progress, animated: !isTransitioning)
}
func hideProgressBar() {
progressBar.isHidden = true
progressBar.setProgress(0, animated: false)
}
func updateReaderModeState(_ state: ReaderModeState) {
locationView.readerModeState = state
}
func setAutocompleteSuggestion(_ suggestion: String?) {
locationTextField?.setAutocompleteSuggestion(suggestion)
}
func setLocation(_ location: String?, search: Bool) {
locationTextField?.text = location
if search, let location = location, !location.isEmpty {
// Not notifying when empty agrees with AutocompleteTextField.textDidChange.
delegate?.urlBar(self, didEnterText: location)
}
}
func enterOverlayMode(_ locationText: String?, pasted: Bool, search: Bool) {
createLocationTextField()
// Show the overlay mode UI, which includes hiding the locationView and replacing it
// with the editable locationTextField.
animateToOverlayState(overlayMode: true)
delegate?.urlBarDidEnterOverlayMode(self)
// Bug 1193755 Workaround - Calling becomeFirstResponder before the animation happens
// won't take the initial frame of the label into consideration, which makes the label
// look squished at the start of the animation and expand to be correct. As a workaround,
// we becomeFirstResponder as the next event on UI thread, so the animation starts before we
// set a first responder.
if pasted {
// Clear any existing text, focus the field, then set the actual pasted text.
// This avoids highlighting all of the text.
self.locationTextField?.text = ""
DispatchQueue.main.async {
self.locationTextField?.becomeFirstResponder()
self.setLocation(locationText, search: search)
}
} else {
// Copy the current URL to the editable text field, then activate it.
self.setLocation(locationText, search: search)
DispatchQueue.main.async {
self.locationTextField?.becomeFirstResponder()
}
}
}
func leaveOverlayMode(didCancel cancel: Bool = false) {
locationTextField?.resignFirstResponder()
animateToOverlayState(overlayMode: false, didCancel: cancel)
delegate?.urlBarDidLeaveOverlayMode(self)
}
func prepareOverlayAnimation() {
// Make sure everything is showing during the transition (we'll hide it afterwards).
self.bringSubview(toFront: self.locationContainer)
self.cancelButton.isHidden = false
self.showQRScannerButton.isHidden = false
self.progressBar.isHidden = false
self.menuButton.isHidden = !self.toolbarIsShowing
self.forwardButton.isHidden = !self.toolbarIsShowing
self.backButton.isHidden = !self.toolbarIsShowing
self.tabsButton.isHidden = !self.toolbarIsShowing || topTabsIsShowing
self.stopReloadButton.isHidden = !self.toolbarIsShowing
}
func transitionToOverlay(_ didCancel: Bool = false) {
self.cancelButton.alpha = inOverlayMode ? 1 : 0
self.showQRScannerButton.alpha = inOverlayMode ? 1 : 0
self.progressBar.alpha = inOverlayMode || didCancel ? 0 : 1
self.tabsButton.alpha = inOverlayMode ? 0 : 1
self.menuButton.alpha = inOverlayMode ? 0 : 1
self.forwardButton.alpha = inOverlayMode ? 0 : 1
self.backButton.alpha = inOverlayMode ? 0 : 1
self.stopReloadButton.alpha = inOverlayMode ? 0 : 1
let borderColor = inOverlayMode ? locationActiveBorderColor : locationBorderColor
locationContainer.layer.borderColor = borderColor.cgColor
if inOverlayMode {
self.line.isHidden = inOverlayMode
// Make the editable text field span the entire URL bar, covering the lock and reader icons.
self.locationTextField?.snp.remakeConstraints { make in
make.edges.equalTo(self.locationView)
}
} else {
// Shrink the editable text field back to the size of the location view before hiding it.
self.locationTextField?.snp.remakeConstraints { make in
make.edges.equalTo(self.locationView.urlTextField)
}
}
}
func updateViewsForOverlayModeAndToolbarChanges() {
self.cancelButton.isHidden = !inOverlayMode
self.showQRScannerButton.isHidden = !inOverlayMode
self.progressBar.isHidden = inOverlayMode
self.menuButton.isHidden = !self.toolbarIsShowing || inOverlayMode
self.forwardButton.isHidden = !self.toolbarIsShowing || inOverlayMode
self.backButton.isHidden = !self.toolbarIsShowing || inOverlayMode
self.tabsButton.isHidden = !self.toolbarIsShowing || inOverlayMode || topTabsIsShowing
self.stopReloadButton.isHidden = !self.toolbarIsShowing || inOverlayMode
}
func animateToOverlayState(overlayMode overlay: Bool, didCancel cancel: Bool = false) {
prepareOverlayAnimation()
layoutIfNeeded()
inOverlayMode = overlay
if !overlay {
removeLocationTextField()
}
UIView.animate(withDuration: 0.3, delay: 0.0, usingSpringWithDamping: 0.85, initialSpringVelocity: 0.0, options: [], animations: { _ in
self.transitionToOverlay(cancel)
self.setNeedsUpdateConstraints()
self.layoutIfNeeded()
}, completion: { _ in
self.updateViewsForOverlayModeAndToolbarChanges()
})
}
func SELdidClickAddTab() {
delegate?.urlBarDidPressTabs(self)
}
func SELdidClickCancel() {
leaveOverlayMode(didCancel: true)
}
func SELtappedScrollToTopArea() {
delegate?.urlBarDidPressScrollToTop(self)
}
}
extension URLBarView: TabToolbarProtocol {
func updateBackStatus(_ canGoBack: Bool) {
backButton.isEnabled = canGoBack
}
func updateForwardStatus(_ canGoForward: Bool) {
forwardButton.isEnabled = canGoForward
}
func updateTabCount(_ count: Int, animated: Bool = true) {
self.tabsButton.updateTabCount(count, animated: animated)
}
func updateReloadStatus(_ isLoading: Bool) {
helper?.updateReloadStatus(isLoading)
if isLoading {
stopReloadButton.setImage(helper?.ImageStop, for: .normal)
} else {
stopReloadButton.setImage(helper?.ImageReload, for: .normal)
}
}
func updatePageStatus(_ isWebPage: Bool) {
stopReloadButton.isEnabled = isWebPage
}
var access: [Any]? {
get {
if inOverlayMode {
guard let locationTextField = locationTextField else { return nil }
return [locationTextField, cancelButton]
} else {
if toolbarIsShowing {
return [backButton, forwardButton, stopReloadButton, locationView, tabsButton, menuButton, progressBar]
} else {
return [locationView, progressBar]
}
}
}
set {
super.accessibilityElements = newValue
}
}
}
extension URLBarView: TabLocationViewDelegate {
func tabLocationViewDidLongPressReaderMode(_ tabLocationView: TabLocationView) -> Bool {
return delegate?.urlBarDidLongPressReaderMode(self) ?? false
}
func tabLocationViewDidTapLocation(_ tabLocationView: TabLocationView) {
guard let (locationText, isSearchQuery) = delegate?.urlBarDisplayTextForURL(locationView.url as URL?) else { return }
var overlayText = locationText
// Make sure to use the result from urlBarDisplayTextForURL as it is responsible for extracting out search terms when on a search page
if let text = locationText, let url = URL(string: text), let host = url.host, AppConstants.MOZ_PUNYCODE {
overlayText = url.absoluteString.replacingOccurrences(of: host, with: host.asciiHostToUTF8())
}
enterOverlayMode(overlayText, pasted: false, search: isSearchQuery)
}
func tabLocationViewDidLongPressLocation(_ tabLocationView: TabLocationView) {
delegate?.urlBarDidLongPressLocation(self)
}
func tabLocationViewDidTapReload(_ tabLocationView: TabLocationView) {
delegate?.urlBarDidPressReload(self)
}
func tabLocationViewDidTapStop(_ tabLocationView: TabLocationView) {
delegate?.urlBarDidPressStop(self)
}
func tabLocationViewDidTapReaderMode(_ tabLocationView: TabLocationView) {
delegate?.urlBarDidPressReaderMode(self)
}
func tabLocationViewDidTapPageOptions(_ tabLocationView: TabLocationView, from button: UIButton) {
delegate?.urlBarDidPressPageOptions(self, from: tabLocationView.pageOptionsButton)
}
func tabLocationViewDidLongPressPageOptions(_ tabLocationView: TabLocationView) {
delegate?.urlBarDidLongPressPageOptions(self, from: tabLocationView.pageOptionsButton)
}
func tabLocationViewLocationAccessibilityActions(_ tabLocationView: TabLocationView) -> [UIAccessibilityCustomAction]? {
return delegate?.urlBarLocationAccessibilityActions(self)
}
}
extension URLBarView: AutocompleteTextFieldDelegate {
func autocompleteTextFieldShouldReturn(_ autocompleteTextField: AutocompleteTextField) -> Bool {
guard let text = locationTextField?.text else { return true }
if !text.trimmingCharacters(in: .whitespaces).isEmpty {
delegate?.urlBar(self, didSubmitText: text)
return true
} else {
return false
}
}
func autocompleteTextField(_ autocompleteTextField: AutocompleteTextField, didEnterText text: String) {
delegate?.urlBar(self, didEnterText: text)
}
func autocompleteTextFieldDidBeginEditing(_ autocompleteTextField: AutocompleteTextField) {
autocompleteTextField.highlightAll()
}
func autocompleteTextFieldShouldClear(_ autocompleteTextField: AutocompleteTextField) -> Bool {
delegate?.urlBar(self, didEnterText: "")
return true
}
}
// MARK: UIAppearance
extension URLBarView {
dynamic var cancelTintColor: UIColor? {
get { return cancelButton.tintColor }
set { return cancelButton.tintColor = newValue }
}
dynamic var showQRButtonTintColor: UIColor? {
get { return showQRScannerButton.tintColor }
set { return showQRScannerButton.tintColor = newValue }
}
}
extension URLBarView: Themeable {
func applyTheme(_ themeName: String) {
locationView.applyTheme(themeName)
locationTextField?.applyTheme(themeName)
guard let theme = URLBarViewUX.Themes[themeName] else {
fatalError("Theme not found")
}
let isPrivate = themeName == Theme.PrivateMode
progressBar.setGradientColors(startColor: UIConstants.LoadingStartColor.color(isPBM: isPrivate), endColor: UIConstants.LoadingEndColor.color(isPBM: isPrivate))
currentTheme = themeName
locationBorderColor = theme.borderColor!
locationActiveBorderColor = theme.activeBorderColor!
cancelTintColor = theme.buttonTintColor
showQRButtonTintColor = theme.buttonTintColor
backgroundColor = theme.backgroundColor
self.actionButtons.forEach { $0.applyTheme(themeName) }
tabsButton.applyTheme(themeName)
line.backgroundColor = UIConstants.URLBarDivider.color(isPBM: isPrivate)
locationContainer.layer.shadowColor = self.locationBorderColor.cgColor
}
}
// We need a subclass so we can setup the shadows correctly
// This subclass creates a strong shadow on the URLBar
class TabLocationContainerView: UIView {
struct LocationContainerUX {
static let CornerRadius: CGFloat = 4
static let ShadowRadius: CGFloat = 2
static let ShadowOpacity: Float = 1
}
override init(frame: CGRect) {
super.init(frame: frame)
let layer = self.layer
layer.cornerRadius = LocationContainerUX.CornerRadius
layer.shadowRadius = LocationContainerUX.ShadowRadius
layer.shadowOpacity = LocationContainerUX.ShadowOpacity
layer.masksToBounds = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
let layer = self.layer
layer.shadowOffset = CGSize(width: 0, height: 1)
// the shadow appears 2px off from the view rect
let shadowLength: CGFloat = 2
let shadowPath = CGRect(x: shadowLength, y: shadowLength, width: layer.frame.width - (shadowLength * 2), height: layer.frame.height - (shadowLength * 2))
layer.shadowPath = UIBezierPath(roundedRect: shadowPath, cornerRadius: layer.cornerRadius).cgPath
super.layoutSubviews()
}
}
class ToolbarTextField: AutocompleteTextField {
static let Themes: [String: Theme] = {
var themes = [String: Theme]()
var theme = Theme()
theme.backgroundColor = UIColor(rgb: 0x636369)
theme.textColor = UIColor.white
theme.buttonTintColor = UIColor.white
theme.highlightColor = UIConstants.PrivateModeInputHighlightColor
themes[Theme.PrivateMode] = theme
theme = Theme()
theme.backgroundColor = .white
theme.textColor = UIColor(rgb: 0x272727)
theme.highlightColor = AutocompleteTextFieldUX.HighlightColor
themes[Theme.NormalMode] = theme
return themes
}()
dynamic var clearButtonTintColor: UIColor? {
didSet {
// Clear previous tinted image that's cache and ask for a relayout
tintedClearImage = nil
setNeedsLayout()
}
}
fileprivate var tintedClearImage: UIImage?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
// Since we're unable to change the tint color of the clear image, we need to iterate through the
// subviews, find the clear button, and tint it ourselves. Thanks to Mikael Hellman for the tip:
// http://stackoverflow.com/questions/27944781/how-to-change-the-tint-color-of-the-clear-button-on-a-uitextfield
for view in subviews as [UIView] {
if let button = view as? UIButton {
if let image = button.image(for: UIControlState()) {
if tintedClearImage == nil {
tintedClearImage = tintImage(image, color: clearButtonTintColor)
}
if button.imageView?.image != tintedClearImage {
button.setImage(tintedClearImage, for: UIControlState())
}
}
}
}
}
fileprivate func tintImage(_ image: UIImage, color: UIColor?) -> UIImage {
guard let color = color else { return image }
let size = image.size
UIGraphicsBeginImageContextWithOptions(size, false, 2)
let context = UIGraphicsGetCurrentContext()!
image.draw(at: CGPoint.zero, blendMode: CGBlendMode.normal, alpha: 1.0)
context.setFillColor(color.cgColor)
context.setBlendMode(CGBlendMode.sourceIn)
context.setAlpha(1.0)
let rect = CGRect(
x: CGPoint.zero.x,
y: CGPoint.zero.y,
width: image.size.width,
height: image.size.height)
context.fill(rect)
let tintedImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return tintedImage
}
}
extension ToolbarTextField: Themeable {
func applyTheme(_ themeName: String) {
guard let theme = ToolbarTextField.Themes[themeName] else {
fatalError("Theme not found")
}
backgroundColor = theme.backgroundColor
textColor = theme.textColor
clearButtonTintColor = theme.buttonTintColor
highlightColor = theme.highlightColor!
}
}