// // PreferenceOptionsView.swift // Privado // // Created by Juraldinio on 12/24/19. // Copyright © 2019 Omicronmedia. All rights reserved. // import Foundation import AppKit protocol PreferenceOptionsViewDelegate: AnyObject { func selected(index: Int, option: PreferenceOptions, enabled: Bool?, optionIndex: Int?) } final class PreferenceOptionsView: NSView, NSComboBoxDelegate, MenuDelegate { enum Constants { enum Geometry { static let checkLeading: CGFloat = 10.0 static let textFieldTop: CGFloat = 20.0 static let textFieldLeading: CGFloat = 0.0 static let textFieldWidth: CGFloat = 55.0 static let dropDownTop: CGFloat = 15.0 static let dropDownLeading: CGFloat = 10.0 static let dropDownHeight: CGFloat = 30.0 static let dropDownWidth: CGFloat = 100.0 } enum Color { static let dropBackground = NSColor(red: 32, green: 40, blue: 100) static let dropTitle = NSColor(red: 122, green: 134, blue: 190) 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 } enum Font { enum Size { static let checkBox: CGFloat = 12.0 static let title: CGFloat = 12 } } static let DeltaRadioTag = 1 } private weak var delegate: PreferenceOptionsViewDelegate? private var index = 0 private(set) var option: PreferenceOptions = .empty private weak var comboboxMenu: NSComboBox? var isEnabled = false { didSet { self.subviews .compactMap { $0 as? NSButton } .filter { $0.bezelStyle != .rounded } .forEach { $0.isEnabled = self.isEnabled } self.subviews .compactMap { $0 as? NSComboBox } .forEach { $0.isEnabled = self.isEnabled } let textFields = self.subviews .compactMap { $0 as? NSTextField } self.textfieldAnimation(isEnabled: self.isEnabled, textFields: textFields) } } // MARK: - Init convenience init(index: Int, option: PreferenceOptions, delegate: PreferenceOptionsViewDelegate?) { self.init(frame: .zero) self.translatesAutoresizingMaskIntoConstraints = false self.index = index self.option = option self.delegate = delegate self.createOption(option) } // MARK: - Private private func createOption(_ option: PreferenceOptions) { switch option { case .empty: break case let .check(identifier, enabled, value): self.createCheck(with: identifier, enabled: enabled, value: value) case let .toggle(identifier, enabled, value): self.createToggle(with: identifier, enabled: enabled, value: value) case let .menu(identifier, title, index, values): self.createMenu(with: identifier, title: title, enabled: index, values: values) case let .radio(values, title, index, radioForm): self.createRadioButtons(values: values, title: title, typeIndex: index, radioForm: radioForm) } } // MARK: - Creations private func createCheck(with identifier: PreferenceOptionsID, enabled: PreferenceOptionsEnabled, value: PreferenceOptionsValue) { let checkBox = NSButton(frame: .zero) checkBox.isEnabled = false checkBox.translatesAutoresizingMaskIntoConstraints = false checkBox.setButtonType(.toggle) checkBox.bezelStyle = .rounded checkBox.title = value.title if let font = checkBox.font { checkBox.font = NSFont(name: font.fontName, size: Constants.Font.Size.checkBox) } checkBox.state = enabled ? .on : .off checkBox.tag = identifier checkBox.target = self checkBox.action = #selector(self.onToggleAction(_:)) self.addSubview(checkBox) NSLayoutConstraint.activate([ checkBox.topAnchor.constraint(equalTo: self.topAnchor), checkBox.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: Constants.Geometry.checkLeading) ]) } private func createToggle(with identifier: PreferenceOptionsID, enabled: PreferenceOptionsEnabled, value: PreferenceOptionsValue) { let checkBox = NSButton(frame: .zero) checkBox.isEnabled = false checkBox.translatesAutoresizingMaskIntoConstraints = false checkBox.setButtonType(.switch) checkBox.bezelStyle = .rounded checkBox.title = value.title if let font = checkBox.font { checkBox.font = NSFont(name: font.fontName, size: Constants.Font.Size.checkBox) } checkBox.state = enabled ? .on : .off checkBox.tag = identifier checkBox.target = self checkBox.action = #selector(self.onToggleAction(_:)) self.addSubview(checkBox) NSLayoutConstraint.activate([ checkBox.topAnchor.constraint(equalTo: self.topAnchor), checkBox.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: Constants.Geometry.checkLeading) ]) } var myMenu: Menu? var dropDownField: DropButton? var values: [PreferenceOptionsValue]? // create custom menu private func createMenu(with identifier: PreferenceOptionsID, title: String, enabled: PreferenceOptionsIndex, values: [PreferenceOptionsValue]) { self.values = values let textField = NSTextField(labelWithString: title) textField.lineBreakMode = .byWordWrapping textField.usesSingleLineMode = true textField.translatesAutoresizingMaskIntoConstraints = false textField.textColor = .white textField.font = NSFont(name: PrivadoConstants.Font.semiBold, size: 12) self.addSubview(textField) let dropDownField = DropButton() dropDownField.title = values[enabled].title dropDownField.translatesAutoresizingMaskIntoConstraints = false let menu = Menu() menu.delegate = self var menuItems = [WindowMenuItem]() values.forEach { menuItems.append(WindowMenuItem($0.title, isSelectable: true, action: { })) } menu.addItems(menuItems) myMenu = menu // dropDownField.backgroundNormalColor = Constants.Color.dropBackground dropDownField.backgroundNormalColor = NSColor(red: 32, green: 40, blue: 100) dropDownField.titleNormalColor = Constants.Color.dropTitle dropDownField.strokeWidth = 1 dropDownField.strokerColor = NSColor(red: 130, green: 136, blue: 216) dropDownField.cornerRadius = 6 dropDownField.font = NSFont.systemFont(ofSize: 18) dropDownField.target = self dropDownField.action = #selector(menuTapped(_:)) dropDownField.imagePosition = .imageRight dropDownField.spacing = 7.0 dropDownField.image = NSImage(named: "arrow") self.dropDownField = dropDownField self.addSubview(dropDownField) NSLayoutConstraint.activate([ textField.topAnchor.constraint(equalTo: self.topAnchor, constant: Constants.Geometry.textFieldTop), textField.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: Constants.Geometry.textFieldLeading), dropDownField.topAnchor.constraint(equalTo: self.topAnchor, constant: Constants.Geometry.dropDownTop), dropDownField.leadingAnchor.constraint(equalTo: textField.trailingAnchor, constant: Constants.Geometry.dropDownLeading), dropDownField.heightAnchor.constraint(equalToConstant: Constants.Geometry.dropDownHeight), dropDownField.widthAnchor.constraint(equalToConstant: Constants.Geometry.dropDownWidth) ]) } // Create custom radio buttons let buttonsContainer = RadioButtonContainer() var isRadio = false private func createRadioButtons(values: [RadioButtonOption], title: PreferenceOptionsTitle, typeIndex: PreferenceOptionsIndex, radioForm: PreferenceRadioBoxForm) { self.isRadio = true var constraints = [NSLayoutConstraint]() let titleView: NSView? if !title.isEmpty { let titleField = NSTextField() titleField.stringValue = title titleField.backgroundColor = .clear titleField.isBezeled = false titleField.isEditable = false titleField.sizeToFit() titleField.translatesAutoresizingMaskIntoConstraints = false titleField.font = NSFont(name: PrivadoConstants.Font.regular, size: Constants.Font.Size.title) titleField.textColor = .white self.addSubview(titleField) titleView = titleField constraints.append(contentsOf: [ titleField.topAnchor.constraint(equalTo: self.topAnchor, constant: 10), titleField.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10) ]) NSLayoutConstraint.activate(constraints) } else { titleView = nil } var view = NSView() values.enumerated().forEach { index, record in view = self.radioButton(index: index, typeIndex: typeIndex, title: record.title, record: record, radioForm: radioForm, after: index == 0 ? titleView : view, constaints: &constraints) } NSLayoutConstraint.activate(constraints) } private func radioButton(index: Int, typeIndex: PreferenceOptionsIndex, title: PreferenceOptionsTitle, record: RadioButtonOption, radioForm: RadioBoxForm, after: NSView?, constaints: inout [NSLayoutConstraint]) -> NSView { let radioButton = RadioButton(frame: .zero, title: title) radioButton.fontSize = 14 radioButton.textFont = NSFont(name: PrivadoConstants.Font.medium, size: 14) radioButton.radioBoxSize = CGSize(width: 20, height: 20) radioButton.textYoffset = 5 self.buttonsContainer.addButton(radioButton) radioButton.tag = index + Constants.DeltaRadioTag radioButton.isOn = radioButton.tag == typeIndex + Constants.DeltaRadioTag radioButton.backgroundColor = Constants.Color.radioBackgroundColor radioButton.innerCircleColor = Constants.Color.radioBackgroundColor radioButton.outerCircleColor = Constants.Color.radioOuterCircleColor radioButton.innerSelectedColor = Constants.Color.radioInnerSelectedColor radioButton.target = self radioButton.action = #selector(self.radioButtonAction(_:)) radioButton.translatesAutoresizingMaskIntoConstraints = false self.addSubview(radioButton) var layout: [NSLayoutConstraint] = [ radioButton.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10), radioButton.widthAnchor.constraint(equalToConstant: 250), radioButton.heightAnchor.constraint(equalToConstant: 25) ] if let view = after { layout.append(radioButton.topAnchor.constraint(equalTo: view.bottomAnchor, constant: 10)) } else if let view = radioButton.superview { layout.append(radioButton.topAnchor.constraint(equalTo: view.topAnchor)) } constaints.append(contentsOf: layout) return radioButton } private func textfieldAnimation(isEnabled: Bool, textFields: [NSTextField]) { guard self.isRadio else { return } NSAnimationContext.runAnimationGroup { (context) in context.duration = 0.3 context.timingFunction = .easeInEaseOut animator().alphaValue = isEnabled ? 1.0 : 0.25 } } // MARK: - Handlers @objc private func radioButtonAction(_ sender: RadioButton) { self.delegate?.selected(index: self.index, option: self.option, enabled: nil, optionIndex: sender.tag - Constants.DeltaRadioTag) } @objc private func onToggleAction(_ sender: NSButton) { self.delegate?.selected(index: self.index, option: self.option, enabled: sender.state == .on, optionIndex: nil) } @objc private func menuTapped(_ sender: NSButton) { switch sender.state { case .on: self.myMenu?.show(from: sender, animated: true) case .off: self.myMenu?.dismiss(animated: true) default: break } } func menuItemDidTap(_ index: Int?) { if index != nil { guard let index = index, let title = values?[index].title else { return } self.dropDownField?.title = title self.dropDownField?.state = dropDownField?.state == .on ? .off : .on self.delegate?.selected(index: self.index, option: self.option, enabled: nil, optionIndex: index) } else { self.dropDownField?.changeState() } } func comboBoxSelectionDidChange(_ notification: Notification) { guard let combobox = self.comboboxMenu , case .menu = self.option else { return } self.delegate?.selected(index: self.index, option: self.option, enabled: nil, optionIndex: combobox.indexOfSelectedItem) } }