// // PreferencesViewController.swift // PrivadoVPN // // Created by Murad Shabanov on 20.07.2021. // Copyright © 2021 Privado LLC. All rights reserved. // import Foundation import AppKit protocol PreferencesViewInput: AnyObject {} protocol PreferencesViewOutput: AnyObject {} final class PreferencesViewController: NSViewController, PreferencesViewInput { typealias HeaderCreationClosure = () -> NSViewController enum Constants { static let columnIdentifier = NSUserInterfaceItemIdentifier("PreferencesSegmentList") static let cellIdentifier = NSUserInterfaceItemIdentifier("PreferencesSegmentCell") } var preferencePanes = [PreferencePane]() { didSet { self.segmentView.reloadData() } } private var headerCreationClosure: HeaderCreationClosure private var output: PreferencesViewOutput? private var activeTabIndex: Int? private let titleText: NSTextField private let segmentView: NSTableView private let controllerView: NSView private var header: NSViewController? private let line: View // MARK: - JSNavigationBarViewControllerProvider var navigationController: JSNavigationController? init(output: PreferencesViewOutput, headerCreation: @escaping HeaderCreationClosure) { self.titleText = NSTextField(frame: .zero) self.segmentView = NSTableView() self.controllerView = NSView(frame: .zero) self.output = output self.headerCreationClosure = headerCreation self.line = View(frame: .zero) super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func loadView() { self.view = BottomView(backgroundColor: NSColor(red: 0, green: 8, blue: 44)) self.view.wantsLayer = true } override func viewDidLoad() { self.configureUI() self.bind() self.layoutView() } override func viewDidAppear() { if let superview = self.view.superview { NSLayoutConstraint.activate([ self.view.topAnchor.constraint(equalTo: superview.topAnchor), self.view.leadingAnchor.constraint(equalTo: superview.leadingAnchor), self.view.trailingAnchor.constraint(equalTo: superview.trailingAnchor), self.view.bottomAnchor.constraint(equalTo: superview.bottomAnchor) ]) } } private func configureUI() { self.titleText.translatesAutoresizingMaskIntoConstraints = false self.titleText.stringValue = NSLocalizedString("preference.identifier.preferences", comment: "") self.titleText.backgroundColor = .clear self.titleText.isBezeled = false self.titleText.isEditable = false self.titleText.sizeToFit() self.titleText.textColor = .white self.titleText.font = NSFont(name: PrivadoConstants.Font.light, size: 13.5) self.segmentView.translatesAutoresizingMaskIntoConstraints = false self.segmentView.backgroundColor = .clear self.segmentView.selectionHighlightStyle = .none self.segmentView.focusRingType = .none self.segmentView.rowSizeStyle = .custom if #available(OSX 11.0, *) { self.segmentView.style = .fullWidth } self.segmentView.cornerView = NSView() self.controllerView.translatesAutoresizingMaskIntoConstraints = false self.controllerView.wantsLayer = true self.line.translatesAutoresizingMaskIntoConstraints = false self.line.viewBackgroundColor = NSColor(red: 21, green: 25, blue: 53) } private func bind() { self.segmentView.delegate = self self.segmentView.dataSource = self let column = NSTableColumn(identifier: Constants.columnIdentifier) column.width = 160 column.isEditable = false self.segmentView.addTableColumn(column) self.view.addSubview(self.segmentView) self.view.addSubview(self.controllerView) self.view.addSubview(self.line) } private func layoutView() { NSLayoutConstraint.activate([ self.segmentView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), self.segmentView.topAnchor.constraint(equalTo: self.view.topAnchor), self.segmentView.widthAnchor.constraint(equalToConstant: 160), // self.line.widthAnchor.constraint(equalToConstant: 1), self.line.heightAnchor.constraint(equalToConstant: 484), self.line.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 20), self.line.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -20), self.line.leadingAnchor.constraint(equalTo: self.segmentView.trailingAnchor), // self.controllerView.topAnchor.constraint(equalTo: self.view.topAnchor), self.controllerView.leadingAnchor.constraint(equalTo: self.line.trailingAnchor), self.controllerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), self.controllerView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) } private func preferencePaneLabels() -> [String] { var labels = [String]() self.preferencePanes.forEach { labels.append($0.preferencePaneIdentifier.rawValue) } return labels } func activateTab(preferenceIdentifier: PreferencePane.Identifier, animated: Bool) { guard let index = (self.preferencePanes.firstIndex { $0.preferencePaneIdentifier == preferenceIdentifier }) else { return self.activateTab(index: 0, animated: animated) } self.animtionTabTranstition(index: index) } func activateTab(index: Int, animated: Bool) { defer { self.activeTabIndex = index } if self.activeTabIndex.isExist { guard index != self.activeTabIndex else { return } self.animtionTabTranstition(index: index) } else { self.immediatelyDisplayTab(index: index) } } func restoreInitialTab() { if !self.activeTabIndex.isExist { self.activateTab(index: 0, animated: false) } } private func animtionTabTranstition(index: Int) { guard let activeTab = self.activeTabIndex else { assertionFailure("animateTabTransition called before a tab was displayed; transition only works from one tab to another") self.immediatelyDisplayTab(index: index) return } let down = index > activeTab let fromViewController = self.preferencePanes[activeTab] let toViewController = self.preferencePanes[index] let viewBounds = self.controllerView.bounds let height = viewBounds.height let duration = 0.3 fromViewController.view.wantsLayer = true toViewController.view.wantsLayer = true let fadeInAnimation = CABasicAnimation(keyPath: "opacity") fadeInAnimation.fromValue = 0.0 fadeInAnimation.toValue = 1.0 fadeInAnimation.duration = duration fadeInAnimation.timingFunction = .easeInEaseOut fadeInAnimation.fillMode = .forwards fadeInAnimation.isRemovedOnCompletion = false let fadeOutAnimation = CABasicAnimation(keyPath: "opacity") fadeOutAnimation.fromValue = 1.0 fadeOutAnimation.toValue = 0.0 fadeOutAnimation.duration = duration fadeOutAnimation.timingFunction = .easeInEaseOut fadeOutAnimation.fillMode = .forwards fadeOutAnimation.isRemovedOnCompletion = false let slideFromBottomAnimation = CABasicAnimation(keyPath: "position.y") slideFromBottomAnimation.fromValue = -height slideFromBottomAnimation.toValue = 0 slideFromBottomAnimation.duration = duration slideFromBottomAnimation.timingFunction = .easeInEaseOut slideFromBottomAnimation.fillMode = .forwards slideFromBottomAnimation.isRemovedOnCompletion = false let slideFromTopToBottom = CABasicAnimation(keyPath: "position.y") slideFromTopToBottom.fromValue = height slideFromTopToBottom.toValue = 0 slideFromTopToBottom.duration = duration slideFromTopToBottom.timingFunction = .easeInEaseOut slideFromTopToBottom.fillMode = .forwards slideFromTopToBottom.isRemovedOnCompletion = false let slideToBottomAnimation = CABasicAnimation(keyPath: "position.y") slideToBottomAnimation.fromValue = 0 slideToBottomAnimation.toValue = -height slideToBottomAnimation.duration = duration slideToBottomAnimation.timingFunction = .easeInEaseOut slideToBottomAnimation.fillMode = .forwards slideToBottomAnimation.isRemovedOnCompletion = false let slideToTopAnimation = CABasicAnimation(keyPath: "position.y") slideToTopAnimation.fromValue = 0 slideToTopAnimation.toValue = height slideToTopAnimation.duration = duration slideToTopAnimation.timingFunction = .easeInEaseOut slideToTopAnimation.fillMode = .forwards slideToTopAnimation.isRemovedOnCompletion = false self.controllerView.addSubview(toViewController.view, positioned: .above, relativeTo: fromViewController.view) CATransaction.begin() CATransaction.setCompletionBlock { fromViewController.view.removeFromSuperview() fromViewController.view.layer?.removeAllAnimations() } fromViewController.view.layer?.add(fadeOutAnimation, forKey: nil) toViewController.view.layer?.add(fadeInAnimation, forKey: nil) fromViewController.view.layer?.add(down ? slideToTopAnimation : slideToBottomAnimation, forKey: nil) toViewController.view.layer?.add(down ? slideFromBottomAnimation : slideFromTopToBottom, forKey: nil) CATransaction.commit() } private func immediatelyDisplayTab(index: Int) { guard let viewController = self.preferencePanes[safe: index] else { return } self.controllerView.addSubview(viewController.view) } private func animate(fromView: NSView?, toView: NSView?, animation: AnimationBlock) { fromView?.wantsLayer = true toView?.wantsLayer = true for animation in animation(fromView, toView).fromViewAnimations { fromView?.layer?.add(animation, forKey: nil) } for animation in animation(fromView, toView).toViewAnimations { toView?.layer?.add(animation, forKey: nil) } } @objc func tabSelected(_ sender: SegmentedControl) { self.activateTab(index: sender.index, animated: true) } } extension PreferencesViewController: NSTableViewDelegate { func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { guard tableView.tableColumns[safe: 0] == tableColumn , tableColumn.isExist else { return nil } guard let title = self.preferencePanes[safe: row]?.preferencePaneIdentifier.rawValue else { return nil } tableView.makeView(withIdentifier: Constants.cellIdentifier, owner: nil) let cell = SegmentCell(frame: .zero) cell.update(with: title) cell.cellIsSelected(self.activeTabIndex == row) return cell } func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool { guard let selectedRow = self.activeTabIndex, row != selectedRow else { return false } guard let previousView = self.segmentView.view(atColumn: 0, row: selectedRow, makeIfNecessary: true) as? SegmentCell, let nextView = self.segmentView.view(atColumn: 0, row: row, makeIfNecessary: true) as? SegmentCell else { return false} previousView.cellIsSelected(false) nextView.cellIsSelected(true) self.activateTab(index: row, animated: true ) return true } func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { return 55 } } extension PreferencesViewController: NSTableViewDataSource { func numberOfRows(in tableView: NSTableView) -> Int { return self.preferencePanes.count } } extension PreferencesViewController: JSNavigationBarViewControllerProvider { func navigationBarViewController() -> NSViewController { if let header = self.header { return header } let header = self.headerCreationClosure() self.header = header return header } }