// // ProtocolPreferencesViewController.swift // Cyberlock // // Created by Jura on 9/23/19. // Copyright © 2019 Omicronmedia. All rights reserved. // import Foundation import AppKit protocol ProtocolPreferencesViewInput: AnyObject { func available(protocols: [ProtocolPreferenceRecord]) func selected(_ record: ProtocolPreferenceRecord) func selection(enabled: Bool) } protocol ProtocolPreferencesViewOutput: AnyObject { var titleLabelText: String { get } func isReady() func select(_ record: ProtocolPreferenceRecord) func select(_ record: ProtocolPreferenceRecord, option: PreferenceOptions, enabled: Bool?, optionIndex: Int?) } final class ProtocolPreferencesViewController: NSViewController, PreferencePane { // swiftlint:disable nesting struct Constants { struct Geometry { static let buttonsTagDelta = 1000 static let leading: CGFloat = 20.0 static let notificationLeading: CGFloat = 5.0 static let recordsSpace: CGFloat = 30.0 static let innerSpace: CGFloat = 4.0 static let leadingDescription: CGFloat = 30.0 static let radioHeight: CGFloat = 30 static let radioWidth: CGFloat = 150 static let titleTrailing: CGFloat = 5 static let titleCenter: CGFloat = 3 static let viewTop: CGFloat = 16 static let viewHeight: CGFloat = 50 static let viewWidth: CGFloat = 200 static let viewLeading: CGFloat = 20 } struct Color { static let radioBackgroundColor = NSColor(red: 45, green: 51, blue: 82) static let radioInnerCircleColor = NSColor(red: 42, green: 216, blue: 153) static let radioOuterCircleColor = NSColor(red: 42, green: 216, blue: 153) static let radioInnerSelectedColor: NSColor = .clear static let description = NSColor(red: 124, green: 134, blue: 186) } struct Font { struct Size { static let title: CGFloat = 16 static let text: CGFloat = 12 } } } // swiftlint:enable nesting private var titleLabel: NSTextField? private var protocols = [ProtocolPreferenceRecord]() private var optionsViews = [Int: [PreferenceOptionsView]]() private let output: ProtocolPreferencesViewOutput private var buttonsContainer = RadioButtonContainer() // MARK: - Init init(output: ProtocolPreferencesViewOutput) { self.output = output super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - Lifecycle override func loadView() { let view = NSView(frame: .zero) view.translatesAutoresizingMaskIntoConstraints = false self.view = view } override func viewDidLoad() { super.viewDidLoad() self.output.isReady() } override func viewDidAppear() { super.viewDidAppear() NSLayoutConstraint.activate([ view.widthAnchor.constraint(equalToConstant: 500), view.heightAnchor.constraint(equalToConstant: 524) ]) } // MARK: - Private private func createRecordRepresentation(index: Int, record: ProtocolPreferenceRecord, after: NSView, constaints: inout [NSLayoutConstraint]) -> NSView { let radioButton = RadioButton(frame: .zero, title: record.title) self.buttonsContainer.addButton(radioButton) radioButton.backgroundColor = Constants.Color.radioBackgroundColor radioButton.innerCircleColor = Constants.Color.radioBackgroundColor radioButton.outerCircleColor = Constants.Color.radioOuterCircleColor radioButton.innerSelectedColor = Constants.Color.radioInnerSelectedColor radioButton.radioBoxSize = CGSize(width: 20, height: 20) radioButton.target = self radioButton.action = #selector(self.radioButtonAction(_:)) radioButton.tag = Constants.Geometry.buttonsTagDelta + index radioButton.translatesAutoresizingMaskIntoConstraints = false radioButton.isEnabled = record.enabled radioButton.textFont = NSFont(name: PrivadoConstants.Font.semiBold, size: Constants.Font.Size.title) radioButton.fontSize = Constants.Font.Size.title radioButton.textYoffset = 2 self.view.addSubview(radioButton) let textField = NSTextField(labelWithString: record.description) textField.isEnabled = record.enabled textField.lineBreakMode = .byWordWrapping textField.usesSingleLineMode = false textField.maximumNumberOfLines = 0 textField.translatesAutoresizingMaskIntoConstraints = false textField.textColor = Constants.Color.description textField.font = NSFont(name: PrivadoConstants.Font.regular, size: Constants.Font.Size.text) self.view.addSubview(textField) let radioTopConstraint = after == self.view ? radioButton.topAnchor.constraint(equalTo: after.topAnchor, constant: Constants.Geometry.recordsSpace) : radioButton.topAnchor.constraint(equalTo: after.bottomAnchor, constant: Constants.Geometry.recordsSpace) var layout: [NSLayoutConstraint] = [ radioTopConstraint, radioButton.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: Constants.Geometry.leading), radioButton.heightAnchor.constraint(equalToConstant: Constants.Geometry.radioHeight), radioButton.widthAnchor.constraint(equalToConstant: Constants.Geometry.radioWidth), textField.topAnchor.constraint(equalTo: radioButton.bottomAnchor, constant: Constants.Geometry.innerSpace), textField.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: Constants.Geometry.leading + 25), textField.trailingAnchor.constraint(equalTo: after.trailingAnchor) ] // Show notification if exists let lastTextField: NSTextField if let notification = record.notification { switch notification { case let .image(named: named, text: text, color: color): lastTextField = NSTextField(labelWithString: text) lastTextField.textColor = NSColor(rgb: color) if let img = NSImage(named: named) { let image = ImageView(image: img) image.translatesAutoresizingMaskIntoConstraints = false self.view.addSubview(image) layout.append(contentsOf: [ image.leadingAnchor.constraint(equalTo: radioButton.leadingAnchor), image.topAnchor.constraint(equalTo: textField.bottomAnchor, constant: Constants.Geometry.viewTop), image.widthAnchor.constraint(equalTo: image.heightAnchor), image.heightAnchor.constraint(equalToConstant: Constants.Geometry.radioHeight), // lastTextField.leadingAnchor.constraint(equalTo: image.trailingAnchor, constant: Constants.Geometry.notificationLeading), lastTextField.topAnchor.constraint(equalTo: image.topAnchor) ]) } else { layout.append(contentsOf: [ lastTextField.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: Constants.Geometry.leading + 25), lastTextField.topAnchor.constraint(equalTo: textField.bottomAnchor, constant: Constants.Geometry.viewTop) ]) } case let .text(text: text, color: color): lastTextField = NSTextField(labelWithString: text) lastTextField.textColor = NSColor(rgb: color) } lastTextField.lineBreakMode = .byWordWrapping lastTextField.usesSingleLineMode = false lastTextField.maximumNumberOfLines = 4 lastTextField.translatesAutoresizingMaskIntoConstraints = false lastTextField.font = NSFont(name: PrivadoConstants.Font.regular, size: Constants.Font.Size.text) self.view.addSubview(lastTextField) layout.append(contentsOf: [ lastTextField.trailingAnchor.constraint(equalTo: textField.trailingAnchor) ]) } else { lastTextField = textField } constaints.append(contentsOf: layout) var optionsLayout = [NSLayoutConstraint]() guard let views = self.createOptionsRepresentation(index: index, options: record.options, constaints: &optionsLayout) else { return lastTextField } views.enumerated().forEach { (index, view) in if index == 0 { optionsLayout.append(contentsOf: [ view.leadingAnchor.constraint(equalTo: lastTextField.leadingAnchor), view.topAnchor.constraint(equalTo: lastTextField.bottomAnchor, constant: Constants.Geometry.viewTop), view.heightAnchor.constraint(equalToConstant: Constants.Geometry.viewHeight), view.widthAnchor.constraint(equalToConstant: Constants.Geometry.viewWidth) ]) } else if index == 1 { optionsLayout.append(contentsOf: [ view.leadingAnchor.constraint(equalTo: views[0].trailingAnchor, constant: Constants.Geometry.viewLeading), view.topAnchor.constraint(equalTo: lastTextField.bottomAnchor, constant: Constants.Geometry.viewTop), view.heightAnchor.constraint(equalToConstant: Constants.Geometry.viewHeight), view.widthAnchor.constraint(equalToConstant: Constants.Geometry.viewWidth) ]) } } views.forEach(self.view.addSubview) constaints.append(contentsOf: optionsLayout) self.optionsViews[index] = views return views.last ?? lastTextField } private func createOptionsRepresentation(index: Int, options: [PreferenceOptions]?, constaints: inout [NSLayoutConstraint]) -> [PreferenceOptionsView]? { guard let options = options else { return nil } return options.map { PreferenceOptionsView(index: index, option: $0, delegate: self) } } private func enableOptions(for record: ProtocolPreferenceRecord) { self.protocols.enumerated().forEach { index, value in guard let views = self.optionsViews[index] else { return } let enable = value == record views.forEach { v in v.isEnabled = enable } } } @objc private func radioButtonAction(_ sender: RadioButton?) { guard let button = sender , let record = self.protocols[safe: (button.tag - Constants.Geometry.buttonsTagDelta)] else { return } self.enableOptions(for: record) self.output.select(record) } // MARK: - PreferencePane var toolbarItemIcon: NSImage { guard let img = NSImage(named: NSImage.networkName) else { return NSImage(size: .zero) } return img } let preferencePaneIdentifier = PreferenceWindowIdentifiers.protocol lazy var preferencePaneTitle = PreferenceWindowIdentifiers.title(for: self.preferencePaneIdentifier) } // MARK: - ProtocolPreferencesViewInput extension ProtocolPreferencesViewController: ProtocolPreferencesViewInput { func selection(enabled: Bool) { let element = (Constants.Geometry.buttonsTagDelta...Constants.Geometry.buttonsTagDelta + self.protocols.count).compactMap { self.view.viewWithTag($0) as? RadioButton } guard enabled else { element.forEach { $0.isEnabled = false } self.optionsViews.forEach { _, values in values.forEach { $0.isEnabled = false } } return } element.forEach { let index = $0.tag - Constants.Geometry.buttonsTagDelta guard let record = self.protocols[safe: index] else { return } $0.isEnabled = record.enabled guard $0.isOn == true , let views = self.optionsViews[index] else { return } views.forEach { $0.isEnabled = record.enabled } } } func available(protocols: [ProtocolPreferenceRecord]) { self.protocols = protocols var constraints = [NSLayoutConstraint]() var view: NSView = self.view self.protocols.enumerated().forEach { index, record in view = self.createRecordRepresentation(index: index, record: record, after: view, constaints: &constraints) } NSLayoutConstraint.activate(constraints) } func selected(_ record: ProtocolPreferenceRecord) { guard let index = self.protocols.firstIndex(where: { $0 == record }) , let button = self.view.viewWithTag(Constants.Geometry.buttonsTagDelta + index) as? RadioButton else { return } self.enableOptions(for: record) button.isOn = true } } // MARK: - ProtocolOptionsViewDelegate extension ProtocolPreferencesViewController: PreferenceOptionsViewDelegate { func selected(index: Int, option: PreferenceOptions, enabled: Bool?, optionIndex: Int?) { guard let record = self.protocols[safe: index] else { return } self.output.select(record, option: option, enabled: enabled, optionIndex: optionIndex) } }