// // InterfaceApplicationSettingsViewController.swift // PrivadoVPN // // Created by Murad Shabanov on 05.10.2021. // Copyright © 2021 Privado LLC. All rights reserved. // import Foundation protocol InterfaceApplicationSettingsViewInput: AnyObject { func available(options: [InterfaceApplicationRecord]) func options(enable: Bool, for type: OptionsPreferenceRecordType) func enableUpdater(enabled: Bool) func enableDock(at index: Int) func enableAutostart(enabled: Bool) func update(version: String?, buildDate: String?, debug: Bool) } protocol InterfaceApplicationSettingsViewOutput: AnyObject { func isReady() func changedUpdater(enabled: Bool) func changedDock(index: Int) // func selectedWindowStyle(optionIndex: Int) func changed(_ record: InterfaceApplicationRecord, selected: Bool) } final class InterfaceApplicationSettingsViewController: NSViewController, InterfaceApplicationSettingsViewInput, PreferencePane { // swiftlint:disable nesting private enum Constants { static let buttonsTagDelta = 1000 enum Localized { static let switchOn = "preference.logging.switchOn" static let switchOff = "preference.logging.switchOff" // static let windowStyle = "preference.interfaceSettings.windowStyle" } enum Geometry { static let topSpace: CGFloat = 20.0 static let innerSpace: CGFloat = 5.0 static let leading: CGFloat = 20.0 static let trailing: CGFloat = 45.0 static let recordsSpace: CGFloat = 12.0 static let separatorHeight: CGFloat = 1.0 static let textTrailing: CGFloat = 20.0 static let viewLeading: CGFloat = 10.0 static let viewTop: CGFloat = 14.0 static let viewHeight: CGFloat = 60.0 static let viewWidth: CGFloat = 300.0 enum Switch { static let width: CGFloat = 50.0 static let height: CGFloat = 25.0 static let borderWidth: CGFloat = 2.0 static let thumbRadiusPadding: CGFloat = 1.5 } } enum Font { enum Size { static let title: CGFloat = 16 static let description: CGFloat = 12 } } enum Color { enum Switch { static let border = NSColor(red: 122, green: 134, blue: 190) static let onTint = NSColor(red: 122, green: 134, blue: 190) static let offTint = NSColor(red: 0, green: 8, blue: 44) static let onText = NSColor(red: 0, green: 8, blue: 44) static let offText = NSColor(red: 122, green: 134, blue: 190) } static let text = NSColor(red: 124, green: 134, blue: 186) static let version = NSColor(red: 122, green: 134, blue: 186) } } // swiftlint:enable nesting private var options = [InterfaceApplicationRecord]() private let output: InterfaceApplicationSettingsViewOutput private var optionsViews = [Int: [PreferenceOptionsView]]() private var versionField: NSTextField? // MARK: - Init init(output: InterfaceApplicationSettingsViewOutput) { 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: - InterfaceApplicationSettingsViewInput func available(options: [InterfaceApplicationRecord]) { self.options = options var constraints = [NSLayoutConstraint]() var view = self.view options.enumerated().forEach { index, record in view = self.createRecordRepresentation(index: index, record: record, after: view, constaints: &constraints) } self.versionField = self.initializeVersionField() if let versionField = self.versionField { constraints.append(contentsOf: [ versionField.topAnchor.constraint(equalTo: view.bottomAnchor, constant: 15), versionField.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 20) ]) } NSLayoutConstraint.activate(constraints) } func options(enable: Bool, for type: OptionsPreferenceRecordType) { } func enableUpdater(enabled: Bool) { guard let optionIndex = self.options.firstIndex(where: { $0.type == .backgroundUpdate }), let views = self.optionsViews[optionIndex] else { return } views.forEach { $0.isEnabled = enabled } } func enableDock(at index: Int) { guard let optionIndex = self.options.firstIndex(where: { $0.type == .dock }), let views = self.optionsViews[optionIndex], let view = views.first(where: { $0.isRadio == true }) else { return } view.buttonsContainer.selectButton(at: index) } func enableAutostart(enabled: Bool) { guard let optionIndex = self.options.firstIndex(where: { $0.type == .autoStart }), let views = self.optionsViews[optionIndex] else { return } views.forEach { $0.isEnabled = enabled } } func update(version: String?, buildDate: String?, debug: Bool = false) { guard let versionString = version else { self.versionField?.isHidden = true return } var fieldValue: String if let buildDate = buildDate { fieldValue = "\(versionString), build date: \(buildDate)" } else { fieldValue = "\(versionString)" } if debug { fieldValue = "DEBUG \(fieldValue)" } self.versionField?.stringValue = fieldValue self.versionField?.isHidden = false } // MARK: - Actions @objc private func actionSwitch(_ sender: Switch?) { guard let switcher = sender else { return } self.output.changedUpdater(enabled: switcher.isOn) } @objc private func checkButtonAction(_ sender: Switch?) { guard let button = sender , let record = self.options[safe: (button.tag - Constants.buttonsTagDelta)] else { return } self.output.changed(record, selected: button.isOn == true) } // MARK: - Private methods private func configureSwitch() -> Switch { let switchButton = Switch() switchButton.translatesAutoresizingMaskIntoConstraints = false switchButton.borderColor = Constants.Color.Switch.border switchButton.onTintColor = Constants.Color.Switch.onTint switchButton.offTintColor = Constants.Color.Switch.offTint switchButton.onTextColor = Constants.Color.Switch.onText switchButton.offTextColor = Constants.Color.Switch.offText switchButton.onText = NSLocalizedString(Constants.Localized.switchOn, comment: "") switchButton.offText = NSLocalizedString(Constants.Localized.switchOff, comment: "") switchButton.borderWidth = Constants.Geometry.Switch.borderWidth switchButton.thumbRadiusPadding = Constants.Geometry.Switch.thumbRadiusPadding switchButton.target = self switchButton.action = #selector(self.checkButtonAction(_:)) return switchButton } private func createRecordRepresentation(index: Int, record: InterfaceApplicationRecord, after: NSView, constaints: inout [NSLayoutConstraint]) -> NSView { let button: Switch? if let selected = record.selected { let switchButton = self.configureSwitch() switchButton.tag = Constants.buttonsTagDelta + index switchButton.isEnabled = record.enabled switchButton.isOn = selected self.view.addSubview(switchButton) button = switchButton } else { button = nil } let titleField = NSTextField(labelWithString: record.title) titleField.isEnabled = record.enabled titleField.usesSingleLineMode = true titleField.translatesAutoresizingMaskIntoConstraints = false titleField.font = NSFont(name: PrivadoConstants.Font.semiBold, size: Constants.Font.Size.title) titleField.textColor = .white self.view.addSubview(titleField) var mainTextField = titleField let descriptionField: NSTextField? if let description = record.description { let textField = NSTextField(labelWithString: description) textField.isEnabled = record.enabled textField.usesSingleLineMode = false textField.translatesAutoresizingMaskIntoConstraints = false textField.lineBreakMode = .byWordWrapping textField.maximumNumberOfLines = 0 textField.font = NSFont(name: PrivadoConstants.Font.regular, size: Constants.Font.Size.description) textField.textColor = Constants.Color.text self.view.addSubview(textField) descriptionField = textField mainTextField = textField } else { descriptionField = nil } var layout = [NSLayoutConstraint]() let separator: NSView? if after != self.view { let separatorView = NSView() separatorView.translatesAutoresizingMaskIntoConstraints = false separatorView.viewBackgroundColor = NSColor(red: 130, green: 136, blue: 216) self.view.addSubview(separatorView) separator = separatorView layout.append(contentsOf: [ separatorView.topAnchor.constraint(equalTo: after.bottomAnchor, constant: Constants.Geometry.recordsSpace), separatorView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: Constants.Geometry.leading), separatorView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -Constants.Geometry.trailing), separatorView.heightAnchor.constraint(equalToConstant: Constants.Geometry.separatorHeight) ]) layout.append(contentsOf: [ titleField.topAnchor.constraint(equalTo: separatorView.bottomAnchor, constant: Constants.Geometry.recordsSpace) ]) } else { separator = nil layout.append(contentsOf: [ titleField.topAnchor.constraint(equalTo: after.topAnchor, constant: Constants.Geometry.topSpace) ]) } layout.append(contentsOf: [ titleField.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: Constants.Geometry.leading) ]) if let textField = descriptionField { layout.append(contentsOf: [ textField.topAnchor.constraint(equalTo: titleField.bottomAnchor, constant: Constants.Geometry.innerSpace), textField.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: Constants.Geometry.leading) ]) } if let switchButton = button { if let separatorView = separator { layout.append(contentsOf: [ switchButton.topAnchor.constraint(equalTo: separatorView.bottomAnchor, constant: Constants.Geometry.topSpace) ]) } else { layout.append(contentsOf: [ switchButton.topAnchor.constraint(equalTo: after.topAnchor, constant: Constants.Geometry.viewTop) ]) } layout.append(contentsOf: [ switchButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -Constants.Geometry.trailing), switchButton.widthAnchor.constraint(equalToConstant: Constants.Geometry.Switch.width), switchButton.heightAnchor.constraint(equalToConstant: Constants.Geometry.Switch.height) ]) } if let textField = descriptionField, let switchButton = button { layout.append( textField.trailingAnchor.constraint(equalTo: switchButton.leadingAnchor, constant: -Constants.Geometry.textTrailing) ) } constaints.append(contentsOf: layout) var optionsLayout = [NSLayoutConstraint]() guard let views = self.createOptionsRepresentation(index: index, options: record.options, constaints: &optionsLayout) else { return mainTextField } views.enumerated().forEach { (index, view) in if index == 0 { optionsLayout.append(contentsOf: [ view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: Constants.Geometry.viewLeading), view.topAnchor.constraint(equalTo: mainTextField.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 if let view = views.last { return view } return mainTextField } private func initializeVersionField() -> NSTextField { let textField = NSTextField(frame: .zero) textField.isBezeled = false textField.drawsBackground = false textField.isEditable = false textField.isSelectable = false textField.translatesAutoresizingMaskIntoConstraints = false textField.focusRingType = .none textField.alignment = .right textField.font = NSFont(name: PrivadoConstants.Font.regular, size: 12) textField.textColor = Constants.Color.version self.view.addSubview(textField) return textField } 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) } } // MARK: - PreferencePane let preferencePaneIdentifier = PreferenceWindowIdentifiers.interface lazy var preferencePaneTitle = PreferenceWindowIdentifiers.title(for: self.preferencePaneIdentifier) } extension InterfaceApplicationSettingsViewController: PreferenceOptionsViewDelegate { func selected(index: Int, option: PreferenceOptions, enabled: Bool?, optionIndex: Int?) { guard let optionIndex = optionIndex else { return } self.output.changedDock(index: optionIndex) } }