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

399 lines
16 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
protocol TabLocationViewDelegate {
func tabLocationViewDidTapLocation(_ tabLocationView: TabLocationView)
func tabLocationViewDidLongPressLocation(_ tabLocationView: TabLocationView)
func tabLocationViewDidTapReaderMode(_ tabLocationView: TabLocationView)
func tabLocationViewDidTapPageOptions(_ tabLocationView: TabLocationView, from button: UIButton)
func tabLocationViewDidLongPressPageOptions(_ tabLocationVIew: TabLocationView)
/// - 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
@discardableResult func tabLocationViewDidLongPressReaderMode(_ tabLocationView: TabLocationView) -> Bool
func tabLocationViewLocationAccessibilityActions(_ tabLocationView: TabLocationView) -> [UIAccessibilityCustomAction]?
}
struct TabLocationViewUX {
static let HostFontColor = UIColor.black
static let BaseURLFontColor = UIColor.gray
static let LocationContentInset = 8
static let URLBarPadding = 4
static let Themes: [String: Theme] = {
var themes = [String: Theme]()
var theme = Theme()
theme.URLFontColor = UIColor.lightGray
theme.textColor = UIColor(rgb: 0xf9f9fa)
theme.highlightButtonColor = UIConstants.PrivateModePurple
theme.buttonTintColor = UIColor(rgb: 0xADADb0)
theme.backgroundColor = UIColor(rgb: 0x636369)
themes[Theme.PrivateMode] = theme
theme = Theme()
theme.textColor = UIColor(rgb: 0x27)
theme.highlightButtonColor = UIColor(rgb: 0x00A2FE)
theme.buttonTintColor = UIColor(rgb: 0x737373)
theme.backgroundColor = .white
themes[Theme.NormalMode] = theme
return themes
}()
}
class TabLocationView: UIView {
var delegate: TabLocationViewDelegate?
var longPressRecognizer: UILongPressGestureRecognizer!
var tapRecognizer: UITapGestureRecognizer!
dynamic var baseURLFontColor: UIColor = TabLocationViewUX.BaseURLFontColor {
didSet { updateTextWithURL() }
}
var url: URL? {
didSet {
let wasHidden = lockImageView.isHidden
lockImageView.isHidden = url?.scheme != "https"
if wasHidden != lockImageView.isHidden {
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil)
}
updateTextWithURL()
pageOptionsButton.isHidden = (url == nil)
setNeedsUpdateConstraints()
}
}
var readerModeState: ReaderModeState {
get {
return readerModeButton.readerModeState
}
set (newReaderModeState) {
if newReaderModeState != self.readerModeButton.readerModeState {
let wasHidden = readerModeButton.isHidden
self.readerModeButton.readerModeState = newReaderModeState
readerModeButton.isHidden = (newReaderModeState == ReaderModeState.unavailable)
separatorLine.isHidden = readerModeButton.isHidden
if wasHidden != readerModeButton.isHidden {
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil)
if !readerModeButton.isHidden {
// Delay the Reader Mode accessibility announcement briefly to prevent interruptions.
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, Strings.ReaderModeAvailableVoiceOverAnnouncement)
}
}
}
UIView.animate(withDuration: 0.1, animations: { () -> Void in
if newReaderModeState == ReaderModeState.unavailable {
self.readerModeButton.alpha = 0.0
} else {
self.readerModeButton.alpha = 1.0
}
self.setNeedsUpdateConstraints()
self.layoutIfNeeded()
})
}
}
}
lazy var placeholder: NSAttributedString = {
let placeholderText = NSLocalizedString("Search or enter address", comment: "The text shown in the URL bar on about:home")
return NSAttributedString(string: placeholderText, attributes: [NSForegroundColorAttributeName: UIColor.gray])
}()
lazy var urlTextField: UITextField = {
let urlTextField = DisplayTextField()
self.longPressRecognizer.delegate = self
urlTextField.addGestureRecognizer(self.longPressRecognizer)
self.tapRecognizer.delegate = self
urlTextField.addGestureRecognizer(self.tapRecognizer)
// Prevent the field from compressing the toolbar buttons on the 4S in landscape.
urlTextField.setContentCompressionResistancePriority(250, for: UILayoutConstraintAxis.horizontal)
urlTextField.attributedPlaceholder = self.placeholder
urlTextField.accessibilityIdentifier = "url"
urlTextField.accessibilityActionsSource = self
urlTextField.font = UIConstants.DefaultChromeFont
urlTextField.backgroundColor = .clear
return urlTextField
}()
fileprivate lazy var lockImageView: UIImageView = {
let lockImageView = UIImageView(image: UIImage.templateImageNamed("lock_verified"))
lockImageView.isHidden = true
lockImageView.tintColor = UIColor(rgb: 0x16DA00)
lockImageView.isAccessibilityElement = true
lockImageView.contentMode = UIViewContentMode.center
lockImageView.accessibilityLabel = NSLocalizedString("Secure connection", comment: "Accessibility label for the lock icon, which is only present if the connection is secure")
return lockImageView
}()
fileprivate lazy var readerModeButton: ReaderModeButton = {
let readerModeButton = ReaderModeButton(frame: CGRect.zero)
readerModeButton.isHidden = true
readerModeButton.addTarget(self, action: #selector(TabLocationView.SELtapReaderModeButton), for: .touchUpInside)
readerModeButton.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(TabLocationView.SELlongPressReaderModeButton(_:))))
readerModeButton.isAccessibilityElement = true
readerModeButton.imageView?.contentMode = UIViewContentMode.scaleAspectFit
readerModeButton.accessibilityLabel = NSLocalizedString("Reader View", comment: "Accessibility label for the Reader View button")
readerModeButton.accessibilityIdentifier = "TabLocationView.readerModeButton"
readerModeButton.accessibilityCustomActions = [UIAccessibilityCustomAction(name: NSLocalizedString("Add to Reading List", comment: "Accessibility label for action adding current page to reading list."), target: self, selector: #selector(TabLocationView.SELreaderModeCustomAction))]
return readerModeButton
}()
lazy var pageOptionsButton: ToolbarButton = {
let pageOptionsButton = ToolbarButton(frame: CGRect.zero)
pageOptionsButton.setImage(UIImage.templateImageNamed("menu-More-Options"), for: .normal)
pageOptionsButton.isHidden = true
pageOptionsButton.addTarget(self, action: #selector(TabLocationView.SELDidPressPageOptionsButton), for: .touchUpInside)
pageOptionsButton.isAccessibilityElement = true
pageOptionsButton.imageView?.contentMode = .center
pageOptionsButton.accessibilityLabel = NSLocalizedString("Page Options Menu", comment: "Accessibility label for the Page Options menu button")
pageOptionsButton.accessibilityIdentifier = "TabLocationView.pageOptionsButton"
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(TabLocationView.SELDidLongPressPageOptionsButton))
pageOptionsButton.addGestureRecognizer(longPressGesture)
return pageOptionsButton
}()
lazy var separatorLine: UIView = {
let line = UIView()
line.layer.cornerRadius = 2
line.backgroundColor = UIColor(rgb: 0xE5E5E5)
line.isHidden = true
return line
}()
override init(frame: CGRect) {
super.init(frame: frame)
longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(TabLocationView.SELlongPressLocation(_:)))
tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(TabLocationView.SELtapLocation(_:)))
addSubview(urlTextField)
addSubview(lockImageView)
addSubview(readerModeButton)
addSubview(pageOptionsButton)
addSubview(separatorLine)
lockImageView.snp.makeConstraints { make in
make.size.equalTo(24)
make.centerY.equalTo(self)
make.leading.equalTo(self).offset(9)
}
pageOptionsButton.snp.makeConstraints { make in
make.centerY.equalTo(self)
make.trailing.equalTo(self)
make.width.equalTo(44)
make.height.equalTo(self)
}
separatorLine.snp.makeConstraints { make in
make.width.equalTo(1)
make.height.equalTo(26)
make.trailing.equalTo(pageOptionsButton.snp.leading)
make.centerY.equalTo(self)
}
readerModeButton.snp.makeConstraints { make in
make.centerY.equalTo(self)
make.trailing.equalTo(separatorLine.snp.leading).offset(-9)
make.size.equalTo(24)
}
}
override var accessibilityElements: [Any]? {
get {
return [lockImageView, urlTextField, readerModeButton, pageOptionsButton].filter { !$0.isHidden }
}
set {
super.accessibilityElements = newValue
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func updateConstraints() {
urlTextField.snp.remakeConstraints { make in
make.top.bottom.equalTo(self)
if lockImageView.isHidden {
make.leading.equalTo(self).offset(TabLocationViewUX.LocationContentInset)
} else {
make.leading.equalTo(self.lockImageView.snp.trailing).offset(TabLocationViewUX.URLBarPadding)
}
if readerModeButton.isHidden {
make.trailing.equalTo(self.pageOptionsButton.snp.leading).offset(-TabLocationViewUX.URLBarPadding)
} else {
make.trailing.equalTo(self.readerModeButton.snp.leading).offset(-TabLocationViewUX.URLBarPadding)
}
}
super.updateConstraints()
}
func SELtapReaderModeButton() {
delegate?.tabLocationViewDidTapReaderMode(self)
}
func SELlongPressReaderModeButton(_ recognizer: UILongPressGestureRecognizer) {
if recognizer.state == UIGestureRecognizerState.began {
delegate?.tabLocationViewDidLongPressReaderMode(self)
}
}
func SELDidPressPageOptionsButton(_ button: UIButton) {
delegate?.tabLocationViewDidTapPageOptions(self, from: button)
}
func SELDidLongPressPageOptionsButton(_ recognizer: UILongPressGestureRecognizer) {
delegate?.tabLocationViewDidLongPressPageOptions(self)
}
func SELlongPressLocation(_ recognizer: UITapGestureRecognizer) {
if recognizer.state == UIGestureRecognizerState.began {
delegate?.tabLocationViewDidLongPressLocation(self)
}
}
func SELtapLocation(_ recognizer: UITapGestureRecognizer) {
delegate?.tabLocationViewDidTapLocation(self)
}
func SELreaderModeCustomAction() -> Bool {
return delegate?.tabLocationViewDidLongPressReaderMode(self) ?? false
}
fileprivate func updateTextWithURL() {
if let host = url?.host, AppConstants.MOZ_PUNYCODE {
urlTextField.text = url?.absoluteString.replacingOccurrences(of: host, with: host.asciiHostToUTF8())
} else {
urlTextField.text = url?.absoluteString
}
// remove https:// (the scheme) from the url when displaying
if let scheme = url?.scheme, let range = url?.absoluteString.range(of: "\(scheme)://") {
urlTextField.text = url?.absoluteString.replacingCharacters(in: range, with: "")
}
}
}
extension TabLocationView: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
// If the longPressRecognizer is active, fail all other recognizers to avoid conflicts.
return gestureRecognizer == longPressRecognizer
}
}
extension TabLocationView: AccessibilityActionsSource {
func accessibilityCustomActionsForView(_ view: UIView) -> [UIAccessibilityCustomAction]? {
if view === urlTextField {
return delegate?.tabLocationViewLocationAccessibilityActions(self)
}
return nil
}
}
extension TabLocationView: Themeable {
func applyTheme(_ themeName: String) {
guard let theme = TabLocationViewUX.Themes[themeName] else {
log.error("Unable to apply unknown theme \(themeName)")
return
}
let isPrivate = themeName == Theme.PrivateMode
backgroundColor = theme.backgroundColor
urlTextField.textColor = theme.textColor
readerModeButton.selectedTintColor = theme.highlightButtonColor
readerModeButton.unselectedTintColor = theme.buttonTintColor
pageOptionsButton.selectedTintColor = theme.highlightButtonColor
pageOptionsButton.unselectedTintColor = isPrivate ? UIColor(rgb: 0xD2d2d4) : UIColor(rgb: 0x272727)
pageOptionsButton.tintColor = pageOptionsButton.unselectedTintColor
separatorLine.backgroundColor = isPrivate ? UIColor(rgb: 0x3f3f43) : UIColor(rgb: 0xE5E5E5)
}
}
class ReaderModeButton: UIButton {
var selectedTintColor: UIColor?
var unselectedTintColor: UIColor?
override init(frame: CGRect) {
super.init(frame: frame)
adjustsImageWhenHighlighted = false
setImage(UIImage.templateImageNamed("reader"), for: .normal)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var isSelected: Bool {
didSet {
self.tintColor = (isHighlighted || isSelected) ? selectedTintColor : unselectedTintColor
}
}
override open var isHighlighted: Bool {
didSet {
self.tintColor = (isHighlighted || isSelected) ? selectedTintColor : unselectedTintColor
}
}
override var tintColor: UIColor! {
didSet {
self.imageView?.tintColor = self.tintColor
}
}
var _readerModeState: ReaderModeState = ReaderModeState.unavailable
var readerModeState: ReaderModeState {
get {
return _readerModeState
}
set (newReaderModeState) {
_readerModeState = newReaderModeState
switch _readerModeState {
case .available:
self.isEnabled = true
self.isSelected = false
case .unavailable:
self.isEnabled = false
self.isSelected = false
case .active:
self.isEnabled = true
self.isSelected = true
}
}
}
}
private class DisplayTextField: UITextField {
weak var accessibilityActionsSource: AccessibilityActionsSource?
override var accessibilityCustomActions: [UIAccessibilityCustomAction]? {
get {
return accessibilityActionsSource?.accessibilityCustomActionsForView(self)
}
set {
super.accessibilityCustomActions = newValue
}
}
fileprivate override var canBecomeFirstResponder: Bool {
return false
}
}