Files
Privado-macOS/macOS/Privado/Sources/Flows/PreferenceFlow/Submodules/Application/View/InterfaceApplicationSettingsViewController.swift

429 lines
16 KiB
Swift

//
// 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)
}
}