CSearchToolbar.swift 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. //
  2. // CSearchToolbar.swift
  3. // ComPDFKit_Tools
  4. //
  5. // Copyright © 2014-2024 PDF Technologies, Inc. All Rights Reserved.
  6. //
  7. // THIS SOURCE CODE AND ANY ACCOMPANYING DOCUMENTATION ARE PROTECTED BY INTERNATIONAL COPYRIGHT LAW
  8. // AND MAY NOT BE RESOLD OR REDISTRIBUTED. USAGE IS BOUND TO THE ComPDFKit LICENSE AGREEMENT.
  9. // UNAUTHORIZED REPRODUCTION OR DISTRIBUTION IS SUBJECT TO CIVIL AND CRIMINAL PENALTIES.
  10. // This notice may not be removed from this file.
  11. //
  12. import UIKit
  13. import ComPDFKit
  14. @objc public protocol CSearchToolbarDelegate: AnyObject {
  15. @objc optional func searchToolbar(_ searchToolbar: CSearchToolbar, onSearchQueryResults results: [Any])
  16. @objc optional func searchToolbarReplace(_ searchToolbar: CSearchToolbar)
  17. @objc optional func searchToolbarChangeSelection(_ searchToolbar: CSearchToolbar, changeSelection selection: CPDFSelection?)
  18. }
  19. public class CSearchToolbar: UIView, UISearchBarDelegate, UITextFieldDelegate {
  20. public weak var pdfView: CPDFListView?
  21. var resultArray: [[CPDFSelection]] = []
  22. public weak var parentVC: UIViewController?
  23. public var searchOption: CPDFSearchOptions = CPDFSearchOptions(rawValue: 0)
  24. public var searchTitleType: CSearchTitleType = .search {
  25. didSet {
  26. if(self.searchTitleType != .replace) {
  27. self.frame = CGRect(x: 0, y: 0, width: self.bounds.size.width, height: 44)
  28. replaceTextFied.isHidden = true
  29. replaceButton.isHidden = true
  30. self.searchButton.isHidden = true
  31. if self.resultArray.count > 0 {
  32. searchListItem.isHidden = false
  33. previousItem.isHidden = false
  34. nextListItem.isHidden = false
  35. } else {
  36. searchListItem.isHidden = true
  37. previousItem.isHidden = true
  38. nextListItem.isHidden = true
  39. }
  40. } else {
  41. self.frame = CGRect(x: 0, y: 0, width: self.bounds.size.width, height: 91)
  42. replaceTextFied.isHidden = false
  43. replaceButton.isHidden = false
  44. searchListItem.isHidden = true
  45. self.searchButton.isHidden = false
  46. if self.resultArray.count > 0 {
  47. searchButton.isHidden = true
  48. replaceButton.isEnabled = true
  49. replaceButton.setTitleColor(CPDFColorUtils.CPageEditToolbarFontColor(), for: .normal)
  50. previousItem.isHidden = false
  51. nextListItem.isHidden = false
  52. } else {
  53. searchButton.isHidden = false
  54. replaceButton.isEnabled = false
  55. replaceButton.setTitleColor(.gray, for: .normal)
  56. previousItem.isHidden = true
  57. nextListItem.isHidden = true
  58. }
  59. }
  60. self.searchBar.becomeFirstResponder()
  61. }
  62. }
  63. public var searchKeyString: String {
  64. get {
  65. return self.searchBar.text ?? ""
  66. }
  67. }
  68. private var searchListItem = UIButton()
  69. private var nextListItem = UIButton()
  70. private var previousItem = UIButton()
  71. var searchBar = UISearchBar()
  72. public var replaceTextFied = UISearchBar()
  73. private var replaceButton = UIButton()
  74. var searchButton = UIButton()
  75. private var nowPageIndex: Int = 0
  76. private var nowNumber: Int = 0
  77. // MARK: - Accessors
  78. private lazy var loadingView: CActivityIndicatorView = {
  79. let loadingView = CActivityIndicatorView(style: .gray)
  80. loadingView.center = window?.center ?? CGPoint.zero
  81. loadingView.autoresizingMask = [.flexibleTopMargin, .flexibleBottomMargin, .flexibleLeftMargin, .flexibleRightMargin]
  82. return loadingView
  83. }()
  84. private var isSearched: Bool = false
  85. weak var delegate: CSearchToolbarDelegate?
  86. // MARK: - Initializers
  87. init(pdfView: CPDFListView) {
  88. self.pdfView = pdfView
  89. super.init(frame: .zero)
  90. self.commonInit()
  91. self.backgroundColor = CPDFColorUtils.CPDFViewControllerBackgroundColor()
  92. }
  93. public override func layoutSubviews() {
  94. super.layoutSubviews()
  95. let offset: CGFloat = 0.0
  96. let offsetX: CGFloat = 10.0
  97. var right:CGFloat = 0.0
  98. if #available(iOS 11.0, *) {
  99. right += self.superview?.safeAreaInsets.right ?? 0
  100. }
  101. let searchBarWidth: CGFloat
  102. if(self.searchTitleType != .replace) {
  103. if !isSearched {
  104. searchBarWidth = (bounds.size.width - right - offsetX * 2)
  105. } else {
  106. searchBarWidth = ((bounds.size.width - right - (offsetX * 2)) - (3 * offset) - 34.0 * 3)
  107. }
  108. let searchListItemX:CGFloat = self.bounds.size.width - right - offsetX - 34.0
  109. searchListItem.frame = CGRect(x: searchListItemX, y: 0, width: 34.0, height: 44)
  110. let nextListItemX:CGFloat = searchListItemX - offset - 34.0
  111. nextListItem.frame = CGRect(x: nextListItemX, y: 0, width: 34.0, height: 44)
  112. let previousItemX:CGFloat = nextListItemX - offset - 34.0
  113. previousItem.frame = CGRect(x: previousItemX, y: 0, width: 34.0, height: 44)
  114. } else {
  115. searchBarWidth = ((bounds.size.width - right - (offsetX * 2)) - (3 * offset) - 34.0 * 2)
  116. let nextListItemX:CGFloat = self.bounds.size.width - right - offsetX - 34.0
  117. nextListItem.frame = CGRect(x: nextListItemX, y: 0, width: 34.0, height: 44)
  118. let previousItemX:CGFloat = nextListItemX - offset - 34.0
  119. previousItem.frame = CGRect(x: previousItemX, y: 0, width: 34.0, height: 44)
  120. let replaceButtonWidth: CGFloat = self.bounds.size.width - previousItemX - offsetX
  121. self.searchButton.frame = CGRect(x: previousItemX, y: 5.0, width: replaceButtonWidth, height: 30.0)
  122. replaceTextFied.frame = CGRect(x: offsetX, y: 51.0, width: searchBarWidth, height: 36.0)
  123. self.replaceButton.frame = CGRect(x: previousItemX, y: 5.0, width: replaceButtonWidth, height: 30.0)
  124. replaceButton.centerY = replaceTextFied.centerY
  125. }
  126. searchBar.frame = CGRect(x: offsetX, y: 4.0, width: searchBarWidth, height: 36.0)
  127. }
  128. required init?(coder: NSCoder) {
  129. fatalError("init(coder:) has not been implemented")
  130. }
  131. // MARK: - Public Methods
  132. public func clearDatas(_ isClearText: Bool) {
  133. self.resultArray = []
  134. if(self.searchTitleType == .replace) {
  135. self.pdfView?.document.cancelFindEditString()
  136. }
  137. self.isSearched = false
  138. if self.searchTitleType == .search {
  139. self.searchButton.isHidden = true
  140. } else if self.searchTitleType == .replace {
  141. self.searchButton.isHidden = false
  142. }
  143. self.previousItem.isHidden = true
  144. self.nextListItem.isHidden = true
  145. self.searchListItem.isHidden = true
  146. self.layoutSubviews()
  147. if isClearText {
  148. self.searchBar.text = ""
  149. self.replaceTextFied.text = ""
  150. }
  151. self.searchButton.setTitleColor(.gray, for: .normal)
  152. self.searchButton.isEnabled = false
  153. self.replaceButton.setTitleColor(.gray, for: .normal)
  154. self.replaceButton.isEnabled = false
  155. self.delegate?.searchToolbarChangeSelection?(self, changeSelection: nil)
  156. }
  157. func show(in subView: UIView) {
  158. subView.addSubview(self)
  159. if(self.searchTitleType != .replace) {
  160. self.frame = CGRect(x: 0, y: 0, width: subView.bounds.size.width, height: 44)
  161. } else {
  162. self.frame = CGRect(x: 0, y: 0, width: subView.bounds.size.width, height: 91)
  163. }
  164. self.autoresizingMask = [.flexibleWidth]
  165. self.searchBar.becomeFirstResponder()
  166. }
  167. func beganSearchText(_ searchText: String) {
  168. if searchText.isEmpty {
  169. return
  170. }
  171. if(self.pdfView?.toolModel == .edit) {
  172. conentEditSearch(searchText)
  173. } else {
  174. preViewSearch(searchText)
  175. }
  176. }
  177. func preViewSearch(_ searchText: String) {
  178. // The search for document characters cannot be repeated
  179. window?.isUserInteractionEnabled = false
  180. if loadingView.superview == nil {
  181. window?.addSubview(loadingView)
  182. }
  183. loadingView.startAnimating()
  184. DispatchQueue.global(qos: .default).async { [weak self] in
  185. guard let self = self else { return }
  186. let results = self.pdfView?.document.find(searchText,with:self.searchOption)
  187. DispatchQueue.main.async { [weak self] in
  188. guard let self = self else { return }
  189. self.window?.isUserInteractionEnabled = true
  190. self.loadingView.stopAnimating()
  191. self.loadingView.removeFromSuperview()
  192. if(results != nil) {
  193. self.resultArray = results!
  194. if results!.count > 0 {
  195. self.isSearched = true
  196. self.previousItem.isHidden = false
  197. self.nextListItem.isHidden = false
  198. self.searchListItem.isHidden = false
  199. self.layoutSubviews()
  200. }
  201. } else {
  202. self.resultArray = []
  203. }
  204. DispatchQueue.main.async { [weak self] in
  205. guard let self = self else { return }
  206. if self.resultArray.count < 1 {
  207. let cancelAction = UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .cancel, handler: nil)
  208. let alert = UIAlertController(title: nil, message: NSLocalizedString("Text not found!", comment: ""), preferredStyle: .alert)
  209. let tRootViewControl = self.parentVC
  210. alert.addAction(cancelAction)
  211. tRootViewControl?.present(alert, animated: true, completion: nil)
  212. } else {
  213. self.nowNumber = 0
  214. self.nowPageIndex = 0
  215. if self.resultArray.count > self.nowPageIndex {
  216. let selections = self.resultArray[self.nowPageIndex]
  217. if selections.count > self.nowNumber {
  218. let selection = selections[self.nowNumber]
  219. self.delegate?.searchToolbarChangeSelection?(self, changeSelection: selection)
  220. }
  221. }
  222. }
  223. }
  224. }
  225. }
  226. }
  227. func conentEditSearch(_ searchText: String) {
  228. // The search for document characters cannot be repeated
  229. window?.isUserInteractionEnabled = false
  230. if loadingView.superview == nil {
  231. window?.addSubview(loadingView)
  232. }
  233. loadingView.startAnimating()
  234. let page = self.pdfView?.document.page(at: 0)
  235. DispatchQueue.global(qos: .default).async { [weak self] in
  236. guard let self = self else { return }
  237. let results = self.pdfView?.document.startFindEditText(from: page, with: searchText, options:self.searchOption)
  238. DispatchQueue.main.async { [weak self] in
  239. guard let self = self else { return }
  240. self.window?.isUserInteractionEnabled = true
  241. self.loadingView.stopAnimating()
  242. self.loadingView.removeFromSuperview()
  243. if(results != nil) {
  244. self.resultArray = results!
  245. if results!.count > 0 {
  246. self.isSearched = true
  247. self.previousItem.isHidden = false
  248. self.nextListItem.isHidden = false
  249. if(self.searchTitleType == .search) {
  250. self.searchListItem .isHidden = false;
  251. }
  252. self.searchButton.isHidden = true
  253. self.layoutSubviews()
  254. }
  255. } else {
  256. self.resultArray = []
  257. }
  258. DispatchQueue.main.async { [weak self] in
  259. guard let self = self else { return }
  260. if self.resultArray.count < 1 {
  261. let cancelAction = UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .cancel, handler: nil)
  262. let alert = UIAlertController(title: nil, message: NSLocalizedString("Text not found!", comment: ""), preferredStyle: .alert)
  263. alert.addAction(cancelAction)
  264. self.parentVC?.present(alert, animated: true, completion: nil)
  265. } else {
  266. self.replaceButton.setTitleColor(CPDFColorUtils.CPageEditToolbarFontColor(), for: .normal)
  267. self.replaceButton.isEnabled = true
  268. self.nowNumber = 0
  269. self.nowPageIndex = 0
  270. if self.resultArray.count > self.nowPageIndex {
  271. let selections = self.resultArray[self.nowPageIndex]
  272. if selections.count > self.nowNumber {
  273. let selection = selections[self.nowNumber]
  274. self.delegate?.searchToolbarChangeSelection?(self, changeSelection: selection)
  275. }
  276. }
  277. }
  278. }
  279. }
  280. }
  281. }
  282. // MARK: - Private Methods
  283. func commonInit() {
  284. addSubview(searchBar)
  285. addSubview(replaceTextFied)
  286. addSubview(replaceButton)
  287. addSubview(searchButton)
  288. addSubview(searchListItem)
  289. addSubview(nextListItem)
  290. addSubview(previousItem)
  291. searchBar.placeholder = NSLocalizedString("Search", comment: "")
  292. replaceTextFied.placeholder = NSLocalizedString("Replace with", comment: "")
  293. previousItem.setImage(UIImage(named: "CPDFSearchImagePrevious", in: Bundle(for: self.classForCoder), compatibleWith: nil), for: .normal)
  294. nextListItem.setImage(UIImage(named: "CPDFSearchImageNext", in: Bundle(for: self.classForCoder), compatibleWith: nil), for: .normal)
  295. searchListItem.setImage(UIImage(named: "CPDFSearchImageList", in: Bundle(for: self.classForCoder), compatibleWith: nil), for: .normal)
  296. previousItem.sizeToFit()
  297. nextListItem.sizeToFit()
  298. searchListItem.sizeToFit()
  299. previousItem.addTarget(self, action: #selector(buttonItemClicked_Previous(_:)), for: .touchUpInside)
  300. nextListItem.addTarget(self, action: #selector(buttonItemClicked_Next(_:)), for: .touchUpInside)
  301. searchListItem.addTarget(self, action: #selector(buttonItemClicked_SearchList(_:)), for: .touchUpInside)
  302. replaceButton.setTitle(NSLocalizedString("Replace All", comment: ""), for: .normal)
  303. replaceButton.titleLabel?.adjustsFontSizeToFitWidth = true
  304. replaceButton.addTarget(self, action: #selector(buttonItemClicked_ReplaceAll(_:)), for: .touchUpInside)
  305. searchButton.setTitle(NSLocalizedString("Search", comment: ""), for: .normal)
  306. searchButton.titleLabel?.adjustsFontSizeToFitWidth = true
  307. searchButton.addTarget(self, action: #selector(buttonItemClicked_Search(_:)), for: .touchUpInside)
  308. replaceButton.setTitleColor(.gray, for: .normal)
  309. replaceButton.isEnabled = false
  310. searchButton.setTitleColor(.gray, for: .normal)
  311. searchButton.isEnabled = false
  312. searchBar.delegate = self
  313. replaceTextFied.delegate = self
  314. previousItem.isHidden = true
  315. nextListItem.isHidden = true
  316. searchListItem.isHidden = true
  317. replaceTextFied.isHidden = true
  318. replaceButton.isHidden = true
  319. searchListItem.isHidden = true
  320. searchButton.isHidden = true
  321. searchBar.searchBarStyle = .minimal
  322. replaceTextFied.searchBarStyle = .minimal
  323. replaceTextFied.setImage(UIImage(), for: .search, state: .normal)
  324. replaceTextFied.returnKeyType = .done
  325. }
  326. // MARK: - Action
  327. @objc func buttonItemClicked_SearchList(_ sender: Any) {
  328. self.delegate?.searchToolbar?(self, onSearchQueryResults: self.resultArray)
  329. }
  330. @objc func buttonItemClicked_Next(_ sender: Any) {
  331. var selection:CPDFSelection?
  332. if(self.pdfView?.toolModel != .edit) {
  333. if nowNumber < (self.resultArray[nowPageIndex].count) - 1 {
  334. nowNumber += 1
  335. } else {
  336. if nowPageIndex >= (resultArray.count ) - 1 {
  337. nowNumber = 0
  338. nowPageIndex = 0
  339. } else {
  340. nowPageIndex += 1
  341. nowNumber = 0
  342. }
  343. }
  344. selection = resultArray[self.nowPageIndex][nowNumber]
  345. } else {
  346. selection = self.pdfView?.document.findBackwordEditText()
  347. }
  348. self.delegate?.searchToolbarChangeSelection?(self, changeSelection: selection)
  349. }
  350. @objc func buttonItemClicked_Previous(_ sender: Any) {
  351. var selection:CPDFSelection?
  352. if(self.pdfView?.toolModel != .edit) {
  353. if nowNumber > 0 {
  354. nowNumber -= 1
  355. } else {
  356. if nowPageIndex == 0 {
  357. nowPageIndex = (resultArray.count) - 1
  358. nowNumber = (resultArray[nowPageIndex].count ) - 1
  359. } else {
  360. nowPageIndex -= 1
  361. nowNumber = (resultArray[nowPageIndex].count ) - 1
  362. }
  363. }
  364. selection = resultArray[self.nowPageIndex][nowNumber]
  365. } else {
  366. selection = self.pdfView?.document.findForwardEditText()
  367. }
  368. self.delegate?.searchToolbarChangeSelection?(self, changeSelection: selection)
  369. }
  370. @objc func buttonItemClicked_ReplaceAll(_ sender: Any) {
  371. guard let replaceString = self.replaceTextFied.text else { return }
  372. if self.loadingView.superview == nil {
  373. self.addSubview(self.loadingView)
  374. }
  375. self.loadingView.startAnimating()
  376. let searchString = self.searchKeyString
  377. self.parentVC?.navigationController?.view.isUserInteractionEnabled = false
  378. DispatchQueue.global(qos: .default).async {
  379. self.pdfView?.document?.replaceAllEditText(with:searchString, toReplace: replaceString)
  380. DispatchQueue.main.async {
  381. self.parentVC?.navigationController?.view.isUserInteractionEnabled = true
  382. self.loadingView.removeFromSuperview()
  383. self.delegate?.searchToolbarReplace?(self)
  384. }
  385. }
  386. }
  387. @objc func buttonItemClicked_Replace(_ sender: Any) {
  388. }
  389. @objc func buttonItemClicked_Search(_ sender: Any) {
  390. searchBar.resignFirstResponder()
  391. let string = searchBar.text
  392. if string?.isEmpty == true {
  393. return
  394. }
  395. self.beganSearchText(string ?? "")
  396. }
  397. // MARK: - UISearchBarDelegate
  398. public func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
  399. searchBar.resignFirstResponder()
  400. if(self.searchBar == searchBar) {
  401. let string = searchBar.text
  402. if string?.isEmpty == true {
  403. return
  404. }
  405. self.beganSearchText(string ?? "")
  406. } else {
  407. if(self.resultArray.count > 0) {
  408. self.searchBar.resignFirstResponder()
  409. }
  410. }
  411. }
  412. public func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
  413. if(self.searchBar == searchBar) {
  414. previousItem.isHidden = true
  415. nextListItem.isHidden = true
  416. searchListItem.isHidden = true
  417. isSearched = false
  418. self.resultArray = []
  419. if(self.searchTitleType == .replace) {
  420. self.pdfView?.document.cancelFindEditString()
  421. searchButton.isHidden = false
  422. if searchText.isEmpty {
  423. self.searchButton.setTitleColor(.gray, for: .normal)
  424. self.searchButton.isEnabled = false
  425. } else {
  426. self.searchButton.setTitleColor(CPDFColorUtils.CPageEditToolbarFontColor(), for: .normal)
  427. self.searchButton.isEnabled = true
  428. }
  429. self.replaceButton.setTitleColor(.gray, for: .normal)
  430. self.replaceButton.isEnabled = false
  431. }
  432. self.delegate?.searchToolbarChangeSelection?(self, changeSelection: nil)
  433. layoutSubviews()
  434. }
  435. }
  436. public func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {
  437. if (self.replaceTextFied == searchBar) {
  438. searchBar.setShowsCancelButton(false, animated: true)
  439. }
  440. return true
  441. }
  442. }