348 lines
14 KiB
Swift
348 lines
14 KiB
Swift
//
|
|
// 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)
|
|
}
|
|
}
|