mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-08-20 19:33:08 +09:00
635 lines
27 KiB
Swift
635 lines
27 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 UIKit
|
|
import Shared
|
|
import Storage
|
|
import Telemetry
|
|
|
|
private enum SearchListSection: Int {
|
|
case searchSuggestions
|
|
case bookmarksAndHistory
|
|
static let Count = 2
|
|
}
|
|
|
|
private struct SearchViewControllerUX {
|
|
static let SearchEngineScrollViewBackgroundColor = UIColor.white.withAlphaComponent(0.8).cgColor
|
|
static let SearchEngineScrollViewBorderColor = UIColor.black.withAlphaComponent(0.2).cgColor
|
|
|
|
// TODO: This should use ToolbarHeight in BVC. Fix this when we create a shared theming file.
|
|
static let EngineButtonHeight: Float = 44
|
|
static let EngineButtonWidth = EngineButtonHeight * 1.4
|
|
static let EngineButtonBackgroundColor = UIColor.clear.cgColor
|
|
|
|
static let SearchImage = "search"
|
|
static let SearchEngineTopBorderWidth = 0.5
|
|
static let SearchImageHeight: Float = 44
|
|
static let SearchImageWidth: Float = 24
|
|
|
|
static let SuggestionBackgroundColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0.8)
|
|
static let SuggestionBorderColor = UIConstants.HighlightBlue
|
|
static let SuggestionBorderWidth: CGFloat = 1
|
|
static let SuggestionCornerRadius: CGFloat = 4
|
|
static let SuggestionInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
|
|
static let SuggestionMargin: CGFloat = 8
|
|
static let SuggestionCellVerticalPadding: CGFloat = 10
|
|
static let SuggestionCellMaxRows = 2
|
|
|
|
static let IconSize: CGFloat = 23
|
|
static let FaviconSize: CGFloat = 29
|
|
static let IconBorderColor = UIColor(white: 0, alpha: 0.1)
|
|
static let IconBorderWidth: CGFloat = 0.5
|
|
}
|
|
|
|
protocol SearchViewControllerDelegate: class {
|
|
func searchViewController(_ searchViewController: SearchViewController, didSelectURL url: URL)
|
|
func searchViewController(_ searchViewController: SearchViewController, didLongPressSuggestion suggestion: String)
|
|
func presentSearchSettingsController()
|
|
}
|
|
|
|
class SearchViewController: SiteTableViewController, KeyboardHelperDelegate, LoaderListener {
|
|
var searchDelegate: SearchViewControllerDelegate?
|
|
|
|
fileprivate let isPrivate: Bool
|
|
fileprivate var suggestClient: SearchSuggestClient?
|
|
|
|
// Views for displaying the bottom scrollable search engine list. searchEngineScrollView is the
|
|
// scrollable container; searchEngineScrollViewContent contains the actual set of search engine buttons.
|
|
fileprivate let searchEngineScrollView = ButtonScrollView()
|
|
fileprivate let searchEngineScrollViewContent = UIView()
|
|
|
|
fileprivate lazy var bookmarkedBadge: UIImage = {
|
|
return UIImage(named: "bookmarked_passive")!
|
|
}()
|
|
|
|
// Cell for the suggestion flow layout. Since heightForHeaderInSection is called *before*
|
|
// cellForRowAtIndexPath, we create the cell to find its height before it's added to the table.
|
|
fileprivate let suggestionCell = SuggestionCell(style: UITableViewCellStyle.default, reuseIdentifier: nil)
|
|
|
|
static var userAgent: String?
|
|
|
|
init(isPrivate: Bool) {
|
|
self.isPrivate = isPrivate
|
|
super.init(nibName: nil, bundle: nil)
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
view.backgroundColor = UIConstants.PanelBackgroundColor
|
|
let blur = UIVisualEffectView(effect: UIBlurEffect(style: UIBlurEffectStyle.light))
|
|
view.addSubview(blur)
|
|
|
|
super.viewDidLoad()
|
|
|
|
KeyboardHelper.defaultHelper.addDelegate(self)
|
|
|
|
searchEngineScrollView.layer.backgroundColor = SearchViewControllerUX.SearchEngineScrollViewBackgroundColor
|
|
searchEngineScrollView.layer.shadowRadius = 0
|
|
searchEngineScrollView.layer.shadowOpacity = 100
|
|
searchEngineScrollView.layer.shadowOffset = CGSize(width: 0, height: -SearchViewControllerUX.SearchEngineTopBorderWidth)
|
|
searchEngineScrollView.layer.shadowColor = SearchViewControllerUX.SearchEngineScrollViewBorderColor
|
|
searchEngineScrollView.clipsToBounds = false
|
|
|
|
searchEngineScrollView.decelerationRate = UIScrollViewDecelerationRateFast
|
|
view.addSubview(searchEngineScrollView)
|
|
|
|
searchEngineScrollViewContent.layer.backgroundColor = UIColor.clear.cgColor
|
|
searchEngineScrollView.addSubview(searchEngineScrollViewContent)
|
|
|
|
layoutTable()
|
|
layoutSearchEngineScrollView()
|
|
|
|
searchEngineScrollViewContent.snp.makeConstraints { make in
|
|
make.center.equalTo(self.searchEngineScrollView).priority(10)
|
|
//left-align the engines on iphones, center on ipad
|
|
if UIScreen.main.traitCollection.horizontalSizeClass == .compact {
|
|
make.left.equalTo(self.searchEngineScrollView).priority(1000)
|
|
} else {
|
|
make.left.greaterThanOrEqualTo(self.searchEngineScrollView).priority(1000)
|
|
}
|
|
make.right.lessThanOrEqualTo(self.searchEngineScrollView).priority(1000)
|
|
make.top.equalTo(self.searchEngineScrollView)
|
|
make.bottom.equalTo(self.searchEngineScrollView)
|
|
}
|
|
|
|
blur.snp.makeConstraints { make in
|
|
make.edges.equalTo(self.view)
|
|
}
|
|
|
|
suggestionCell.delegate = self
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(SearchViewController.SELDynamicFontChanged(_:)), name: NotificationDynamicFontChanged, object: nil)
|
|
}
|
|
|
|
func SELDynamicFontChanged(_ notification: Notification) {
|
|
guard notification.name == NotificationDynamicFontChanged else { return }
|
|
|
|
reloadData()
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
reloadSearchEngines()
|
|
reloadData()
|
|
}
|
|
|
|
fileprivate func layoutSearchEngineScrollView() {
|
|
let keyboardHeight = KeyboardHelper.defaultHelper.currentState?.intersectionHeightForView(self.view) ?? 0
|
|
searchEngineScrollView.snp.remakeConstraints { make in
|
|
make.left.right.equalTo(self.view)
|
|
make.bottom.equalTo(self.view).offset(-keyboardHeight)
|
|
}
|
|
}
|
|
|
|
var searchEngines: SearchEngines! {
|
|
didSet {
|
|
suggestClient?.cancelPendingRequest()
|
|
|
|
// Query and reload the table with new search suggestions.
|
|
querySuggestClient()
|
|
|
|
// Show the default search engine first.
|
|
if !isPrivate {
|
|
let ua = SearchViewController.userAgent as String! ?? "FxSearch"
|
|
suggestClient = SearchSuggestClient(searchEngine: searchEngines.defaultEngine, userAgent: ua)
|
|
}
|
|
|
|
// Reload the footer list of search engines.
|
|
reloadSearchEngines()
|
|
}
|
|
}
|
|
|
|
fileprivate var quickSearchEngines: [OpenSearchEngine] {
|
|
var engines = searchEngines.quickSearchEngines
|
|
|
|
// If we're not showing search suggestions, the default search engine won't be visible
|
|
// at the top of the table. Show it with the others in the bottom search bar.
|
|
if isPrivate || !searchEngines.shouldShowSearchSuggestions {
|
|
engines?.insert(searchEngines.defaultEngine, at: 0)
|
|
}
|
|
|
|
return engines!
|
|
}
|
|
|
|
var searchQuery: String = "" {
|
|
didSet {
|
|
// Reload the tableView to show the updated text in each engine.
|
|
reloadData()
|
|
}
|
|
}
|
|
|
|
override func reloadData() {
|
|
querySuggestClient()
|
|
}
|
|
|
|
fileprivate func layoutTable() {
|
|
tableView.snp.remakeConstraints { make in
|
|
make.top.equalTo(self.view.snp.top)
|
|
make.leading.trailing.equalTo(self.view)
|
|
make.bottom.equalTo(self.searchEngineScrollView.snp.top)
|
|
}
|
|
}
|
|
|
|
fileprivate func reloadSearchEngines() {
|
|
searchEngineScrollViewContent.subviews.forEach { $0.removeFromSuperview() }
|
|
var leftEdge = searchEngineScrollViewContent.snp.left
|
|
|
|
//search settings icon
|
|
let searchButton = UIButton()
|
|
searchButton.setImage(UIImage(named: "quickSearch"), for: UIControlState())
|
|
searchButton.imageView?.contentMode = UIViewContentMode.center
|
|
searchButton.layer.backgroundColor = SearchViewControllerUX.EngineButtonBackgroundColor
|
|
searchButton.addTarget(self, action: #selector(SearchViewController.SELdidClickSearchButton), for: UIControlEvents.touchUpInside)
|
|
searchButton.accessibilityLabel = String(format: NSLocalizedString("Search Settings", tableName: "Search", comment: "Label for search settings button."))
|
|
|
|
searchButton.imageView?.snp.makeConstraints { make in
|
|
make.width.height.equalTo(SearchViewControllerUX.SearchImageWidth)
|
|
return
|
|
}
|
|
|
|
searchEngineScrollViewContent.addSubview(searchButton)
|
|
searchButton.snp.makeConstraints { make in
|
|
make.size.equalTo(SearchViewControllerUX.FaviconSize)
|
|
//offset the left edge to align with search results
|
|
make.left.equalTo(leftEdge).offset(SearchViewControllerUX.SuggestionMargin * 2)
|
|
make.top.equalTo(self.searchEngineScrollViewContent).offset(SearchViewControllerUX.SuggestionMargin)
|
|
make.bottom.equalTo(self.searchEngineScrollViewContent).offset(-SearchViewControllerUX.SuggestionMargin)
|
|
}
|
|
|
|
//search engines
|
|
leftEdge = searchButton.snp.right
|
|
for engine in quickSearchEngines {
|
|
let engineButton = UIButton()
|
|
engineButton.setImage(engine.image, for: UIControlState())
|
|
engineButton.imageView?.contentMode = UIViewContentMode.scaleAspectFit
|
|
engineButton.layer.backgroundColor = SearchViewControllerUX.EngineButtonBackgroundColor
|
|
engineButton.addTarget(self, action: #selector(SearchViewController.SELdidSelectEngine(_:)), for: UIControlEvents.touchUpInside)
|
|
engineButton.accessibilityLabel = String(format: NSLocalizedString("%@ search", tableName: "Search", comment: "Label for search engine buttons. The argument corresponds to the name of the search engine."), engine.shortName)
|
|
|
|
engineButton.imageView?.snp.makeConstraints { make in
|
|
make.width.height.equalTo(SearchViewControllerUX.FaviconSize)
|
|
return
|
|
}
|
|
|
|
searchEngineScrollViewContent.addSubview(engineButton)
|
|
engineButton.snp.makeConstraints { make in
|
|
make.width.equalTo(SearchViewControllerUX.EngineButtonWidth)
|
|
make.height.equalTo(SearchViewControllerUX.EngineButtonHeight)
|
|
make.left.equalTo(leftEdge)
|
|
make.top.equalTo(self.searchEngineScrollViewContent)
|
|
make.bottom.equalTo(self.searchEngineScrollViewContent)
|
|
if engine === self.searchEngines.quickSearchEngines.last {
|
|
make.right.equalTo(self.searchEngineScrollViewContent)
|
|
}
|
|
}
|
|
leftEdge = engineButton.snp.right
|
|
}
|
|
}
|
|
|
|
func SELdidSelectEngine(_ sender: UIButton) {
|
|
// The UIButtons are the same cardinality and order as the array of quick search engines.
|
|
// Subtract 1 from index to account for magnifying glass accessory.
|
|
guard let index = searchEngineScrollViewContent.subviews.index(of: sender) else {
|
|
assertionFailure()
|
|
return
|
|
}
|
|
|
|
let engine = quickSearchEngines[index - 1]
|
|
|
|
guard let url = engine.searchURLForQuery(searchQuery) else {
|
|
assertionFailure()
|
|
return
|
|
}
|
|
|
|
Telemetry.default.recordSearch(location: .quickSearch, searchEngine: engine.engineID ?? "other")
|
|
|
|
searchDelegate?.searchViewController(self, didSelectURL: url)
|
|
}
|
|
|
|
func SELdidClickSearchButton() {
|
|
self.searchDelegate?.presentSearchSettingsController()
|
|
}
|
|
|
|
func keyboardHelper(_ keyboardHelper: KeyboardHelper, keyboardWillShowWithState state: KeyboardState) {
|
|
animateSearchEnginesWithKeyboard(state)
|
|
}
|
|
|
|
func keyboardHelper(_ keyboardHelper: KeyboardHelper, keyboardDidShowWithState state: KeyboardState) {
|
|
}
|
|
|
|
func keyboardHelper(_ keyboardHelper: KeyboardHelper, keyboardWillHideWithState state: KeyboardState) {
|
|
animateSearchEnginesWithKeyboard(state)
|
|
}
|
|
|
|
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
|
super.viewWillTransition(to: size, with: coordinator)
|
|
// The height of the suggestions row may change, so call reloadData() to recalculate cell heights.
|
|
coordinator.animate(alongsideTransition: { _ in
|
|
self.tableView.reloadData()
|
|
}, completion: nil)
|
|
}
|
|
|
|
fileprivate func animateSearchEnginesWithKeyboard(_ keyboardState: KeyboardState) {
|
|
layoutSearchEngineScrollView()
|
|
|
|
UIView.animate(withDuration: keyboardState.animationDuration, animations: {
|
|
UIView.setAnimationCurve(keyboardState.animationCurve)
|
|
self.view.layoutIfNeeded()
|
|
})
|
|
}
|
|
|
|
fileprivate func querySuggestClient() {
|
|
suggestClient?.cancelPendingRequest()
|
|
|
|
if searchQuery.isEmpty || !searchEngines.shouldShowSearchSuggestions || searchQuery.looksLikeAURL() {
|
|
suggestionCell.suggestions = []
|
|
tableView.reloadData()
|
|
return
|
|
}
|
|
|
|
suggestClient?.query(searchQuery, callback: { suggestions, error in
|
|
if let error = error {
|
|
let isSuggestClientError = error.domain == SearchSuggestClientErrorDomain
|
|
|
|
switch error.code {
|
|
case NSURLErrorCancelled where error.domain == NSURLErrorDomain:
|
|
// Request was cancelled. Do nothing.
|
|
break
|
|
case SearchSuggestClientErrorInvalidEngine where isSuggestClientError:
|
|
// Engine does not support search suggestions. Do nothing.
|
|
break
|
|
case SearchSuggestClientErrorInvalidResponse where isSuggestClientError:
|
|
print("Error: Invalid search suggestion data")
|
|
default:
|
|
print("Error: \(error.description)")
|
|
}
|
|
} else {
|
|
self.suggestionCell.suggestions = suggestions!
|
|
}
|
|
|
|
// If there are no suggestions, just use whatever the user typed.
|
|
if suggestions?.isEmpty ?? true {
|
|
self.suggestionCell.suggestions = [self.searchQuery]
|
|
}
|
|
|
|
// Reload the tableView to show the new list of search suggestions.
|
|
self.tableView.reloadData()
|
|
})
|
|
}
|
|
|
|
func loader(dataLoaded data: Cursor<Site>) {
|
|
self.data = data
|
|
tableView.reloadData()
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, didSelectRowAtIndexPath indexPath: IndexPath) {
|
|
let section = SearchListSection(rawValue: indexPath.section)!
|
|
if section == SearchListSection.bookmarksAndHistory {
|
|
if let site = data[indexPath.row] {
|
|
if let url = URL(string: site.url) {
|
|
searchDelegate?.searchViewController(self, didSelectURL: url)
|
|
UnifiedTelemetry.recordEvent(category: .action, method: .open, object: .bookmark, value: .awesomebarResults)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
|
if let currentSection = SearchListSection(rawValue: indexPath.section) {
|
|
switch currentSection {
|
|
case .searchSuggestions:
|
|
// heightForRowAtIndexPath is called *before* the cell is created, so to get the height,
|
|
// force a layout pass first.
|
|
suggestionCell.layoutIfNeeded()
|
|
return suggestionCell.frame.height
|
|
default:
|
|
return super.tableView(tableView, heightForRowAt: indexPath)
|
|
}
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
|
return 0
|
|
}
|
|
|
|
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
switch SearchListSection(rawValue: indexPath.section)! {
|
|
case .searchSuggestions:
|
|
suggestionCell.imageView?.image = searchEngines.defaultEngine.image
|
|
suggestionCell.imageView?.isAccessibilityElement = true
|
|
suggestionCell.imageView?.accessibilityLabel = String(format: NSLocalizedString("Search suggestions from %@", tableName: "Search", comment: "Accessibility label for image of default search engine displayed left to the actual search suggestions from the engine. The parameter substituted for \"%@\" is the name of the search engine. E.g.: Search suggestions from Google"), searchEngines.defaultEngine.shortName)
|
|
return suggestionCell
|
|
|
|
case .bookmarksAndHistory:
|
|
let cell = super.tableView(tableView, cellForRowAt: indexPath)
|
|
if let site = data[indexPath.row] {
|
|
if let cell = cell as? TwoLineTableViewCell {
|
|
let isBookmark = site.bookmarked ?? false
|
|
cell.setLines(site.title, detailText: site.url)
|
|
cell.setRightBadge(isBookmark ? self.bookmarkedBadge : nil)
|
|
cell.imageView!.layer.borderColor = SearchViewControllerUX.IconBorderColor.cgColor
|
|
cell.imageView!.layer.borderWidth = SearchViewControllerUX.IconBorderWidth
|
|
cell.imageView?.setIcon(site.icon, forURL: site.tileURL, completed: { (color, url) in
|
|
if site.tileURL == url {
|
|
cell.imageView?.image = cell.imageView?.image?.createScaled(CGSize(width: SearchViewControllerUX.IconSize, height: SearchViewControllerUX.IconSize))
|
|
cell.imageView?.contentMode = .center
|
|
cell.imageView?.backgroundColor = color
|
|
}
|
|
})
|
|
}
|
|
}
|
|
return cell
|
|
}
|
|
}
|
|
|
|
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
switch SearchListSection(rawValue: section)! {
|
|
case .searchSuggestions:
|
|
return searchEngines.shouldShowSearchSuggestions && !searchQuery.looksLikeAURL() && !isPrivate ? 1 : 0
|
|
case .bookmarksAndHistory:
|
|
return data.count
|
|
}
|
|
}
|
|
|
|
func numberOfSectionsInTableView(_ tableView: UITableView) -> Int {
|
|
return SearchListSection.Count
|
|
}
|
|
}
|
|
|
|
extension SearchViewController: SuggestionCellDelegate {
|
|
fileprivate func suggestionCell(_ suggestionCell: SuggestionCell, didSelectSuggestion suggestion: String) {
|
|
// Assume that only the default search engine can provide search suggestions.
|
|
let engine = searchEngines.defaultEngine
|
|
|
|
var url = URIFixup.getURL(suggestion)
|
|
if url == nil {
|
|
url = engine.searchURLForQuery(suggestion)
|
|
}
|
|
|
|
Telemetry.default.recordSearch(location: .suggestion, searchEngine: engine.engineID ?? "other")
|
|
|
|
if let url = url {
|
|
searchDelegate?.searchViewController(self, didSelectURL: url)
|
|
}
|
|
}
|
|
|
|
fileprivate func suggestionCell(_ suggestionCell: SuggestionCell, didLongPressSuggestion suggestion: String) {
|
|
searchDelegate?.searchViewController(self, didLongPressSuggestion: suggestion)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Private extension containing string operations specific to this view controller
|
|
*/
|
|
fileprivate extension String {
|
|
func looksLikeAURL() -> Bool {
|
|
// The assumption here is that if the user is typing in a forward slash and there are no spaces
|
|
// involved, it's going to be a URL. If we type a space, any url would be invalid.
|
|
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1192155 for additional details.
|
|
return self.contains("/") && !self.contains(" ")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* UIScrollView that prevents buttons from interfering with scroll.
|
|
*/
|
|
fileprivate class ButtonScrollView: UIScrollView {
|
|
fileprivate override func touchesShouldCancel(in view: UIView) -> Bool {
|
|
return true
|
|
}
|
|
}
|
|
|
|
fileprivate protocol SuggestionCellDelegate: class {
|
|
func suggestionCell(_ suggestionCell: SuggestionCell, didSelectSuggestion suggestion: String)
|
|
func suggestionCell(_ suggestionCell: SuggestionCell, didLongPressSuggestion suggestion: String)
|
|
}
|
|
|
|
/**
|
|
* Cell that wraps a list of search suggestion buttons.
|
|
*/
|
|
fileprivate class SuggestionCell: UITableViewCell {
|
|
weak var delegate: SuggestionCellDelegate?
|
|
let container = UIView()
|
|
|
|
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
|
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
|
|
isAccessibilityElement = false
|
|
accessibilityLabel = nil
|
|
layoutMargins = UIEdgeInsets.zero
|
|
separatorInset = UIEdgeInsets.zero
|
|
selectionStyle = UITableViewCellSelectionStyle.none
|
|
|
|
container.backgroundColor = UIColor.clear
|
|
contentView.backgroundColor = UIColor.clear
|
|
backgroundColor = UIColor.clear
|
|
contentView.addSubview(container)
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
var suggestions: [String] = [] {
|
|
didSet {
|
|
for view in container.subviews {
|
|
view.removeFromSuperview()
|
|
}
|
|
|
|
for suggestion in suggestions {
|
|
let button = SuggestionButton()
|
|
button.setTitle(suggestion, for: UIControlState())
|
|
button.addTarget(self, action: #selector(SuggestionCell.SELdidSelectSuggestion(_:)), for: UIControlEvents.touchUpInside)
|
|
button.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(SuggestionCell.SELdidLongPressSuggestion(_:))))
|
|
|
|
// If this is the first image, add the search icon.
|
|
if container.subviews.isEmpty {
|
|
let image = UIImage(named: SearchViewControllerUX.SearchImage)
|
|
button.setImage(image, for: UIControlState())
|
|
button.titleEdgeInsets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 0)
|
|
}
|
|
|
|
container.addSubview(button)
|
|
}
|
|
|
|
setNeedsLayout()
|
|
}
|
|
}
|
|
|
|
@objc
|
|
func SELdidSelectSuggestion(_ sender: UIButton) {
|
|
delegate?.suggestionCell(self, didSelectSuggestion: sender.titleLabel!.text!)
|
|
}
|
|
|
|
@objc
|
|
func SELdidLongPressSuggestion(_ recognizer: UILongPressGestureRecognizer) {
|
|
if recognizer.state == UIGestureRecognizerState.began {
|
|
if let button = recognizer.view as! UIButton? {
|
|
delegate?.suggestionCell(self, didLongPressSuggestion: button.titleLabel!.text!)
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
|
|
// The left bounds of the suggestions, aligned with where text would be displayed.
|
|
let textLeft: CGFloat = 61
|
|
|
|
// The maximum width of the container, after which suggestions will wrap to the next line.
|
|
let maxWidth = contentView.frame.width
|
|
|
|
let imageSize = CGFloat(SearchViewControllerUX.FaviconSize)
|
|
|
|
// The height of the suggestions container (minus margins), used to determine the frame.
|
|
// We set it to imageSize.height as a minimum since we don't want the cell to be shorter than the icon
|
|
var height: CGFloat = imageSize
|
|
|
|
var currentLeft = textLeft
|
|
var currentTop = SearchViewControllerUX.SuggestionCellVerticalPadding
|
|
var currentRow = 0
|
|
|
|
for view in container.subviews {
|
|
let button = view as! UIButton
|
|
var buttonSize = button.intrinsicContentSize
|
|
|
|
// Update our base frame height by the max size of either the image or the button so we never
|
|
// make the cell smaller than any of the two
|
|
if height == imageSize {
|
|
height = max(buttonSize.height, imageSize)
|
|
}
|
|
|
|
var width = currentLeft + buttonSize.width + SearchViewControllerUX.SuggestionMargin
|
|
if width > maxWidth {
|
|
// Only move to the next row if there's already a suggestion on this row.
|
|
// Otherwise, the suggestion is too big to fit and will be resized below.
|
|
if currentLeft > textLeft {
|
|
currentRow += 1
|
|
if currentRow >= SearchViewControllerUX.SuggestionCellMaxRows {
|
|
// Don't draw this button if it doesn't fit on the row.
|
|
button.frame = CGRect.zero
|
|
continue
|
|
}
|
|
|
|
currentLeft = textLeft
|
|
currentTop += buttonSize.height + SearchViewControllerUX.SuggestionMargin
|
|
height += buttonSize.height + SearchViewControllerUX.SuggestionMargin
|
|
width = currentLeft + buttonSize.width + SearchViewControllerUX.SuggestionMargin
|
|
}
|
|
|
|
// If the suggestion is too wide to fit on its own row, shrink it.
|
|
if width > maxWidth {
|
|
buttonSize.width = maxWidth - currentLeft - SearchViewControllerUX.SuggestionMargin
|
|
}
|
|
}
|
|
|
|
button.frame = CGRect(x: currentLeft, y: currentTop, width: buttonSize.width, height: buttonSize.height)
|
|
currentLeft += buttonSize.width + SearchViewControllerUX.SuggestionMargin
|
|
}
|
|
|
|
frame.size.height = height + 2 * SearchViewControllerUX.SuggestionCellVerticalPadding
|
|
contentView.frame = bounds
|
|
container.frame = bounds
|
|
|
|
let imageX = (textLeft - imageSize) / 2
|
|
let imageY = (frame.size.height - imageSize) / 2
|
|
imageView!.frame = CGRect(x: imageX, y: imageY, width: imageSize, height: imageSize)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rounded search suggestion button that highlights when selected.
|
|
*/
|
|
fileprivate class SuggestionButton: InsetButton {
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
setTitleColor(UIConstants.HighlightBlue, for: UIControlState())
|
|
setTitleColor(UIColor.white, for: UIControlState.highlighted)
|
|
titleLabel?.font = DynamicFontHelper.defaultHelper.DefaultMediumFont
|
|
backgroundColor = SearchViewControllerUX.SuggestionBackgroundColor
|
|
layer.borderColor = SearchViewControllerUX.SuggestionBorderColor.cgColor
|
|
layer.borderWidth = SearchViewControllerUX.SuggestionBorderWidth
|
|
layer.cornerRadius = SearchViewControllerUX.SuggestionCornerRadius
|
|
contentEdgeInsets = SearchViewControllerUX.SuggestionInsets
|
|
|
|
accessibilityHint = NSLocalizedString("Searches for the suggestion", comment: "Accessibility hint describing the action performed when a search suggestion is clicked")
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
@objc
|
|
override var isHighlighted: Bool {
|
|
didSet {
|
|
backgroundColor = isHighlighted ? UIConstants.HighlightBlue : SearchViewControllerUX.SuggestionBackgroundColor
|
|
}
|
|
}
|
|
}
|