forked from I2P_Developers/i2p.i2p
Mac OSX Launcher: EditorTableView for the 2019 redesign/UI improvements.
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// EditorTableCell.swift
|
||||
// I2PLauncher
|
||||
//
|
||||
// Created by Mikal Villa on 08/04/2019.
|
||||
// Copyright © 2019 The I2P Project. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import SnapKit
|
||||
|
||||
class EditorTableCell: NSTableCellView {
|
||||
let toggleButton = NSButton()
|
||||
var selected: Bool = false {
|
||||
didSet {
|
||||
setNeedsDisplay(frame)
|
||||
}
|
||||
}
|
||||
|
||||
var toggleCallback: () -> Void = {}
|
||||
|
||||
override init(frame frameRect: NSRect) {
|
||||
super.init(frame: frameRect)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
private func commonInit() {
|
||||
let textField = NSTextField()
|
||||
textField.isEditable = false
|
||||
textField.isBordered = false
|
||||
textField.isSelectable = false
|
||||
self.textField = textField
|
||||
let font = NSFont.systemFont(ofSize: 11)
|
||||
textField.font = font
|
||||
textField.textColor = NSColor.textColor
|
||||
textField.backgroundColor = NSColor.clear
|
||||
addSubview(textField)
|
||||
|
||||
textField.snp.makeConstraints { make in
|
||||
make.left.equalTo(10)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
|
||||
addSubview(toggleButton)
|
||||
toggleButton.title = ""
|
||||
toggleButton.isBordered = false
|
||||
toggleButton.bezelStyle = .texturedSquare
|
||||
toggleButton.controlSize = .small
|
||||
toggleButton.target = self
|
||||
toggleButton.action = #selector(EditorTableCell.toggle)
|
||||
toggleButton.wantsLayer = true
|
||||
toggleButton.layer?.borderWidth = 1
|
||||
toggleButton.layer?.cornerRadius = 4
|
||||
toggleButton.snp.makeConstraints { make in
|
||||
make.left.equalTo(textField.snp.right).offset(-4)
|
||||
make.width.equalTo(36)
|
||||
make.right.equalTo(-10)
|
||||
make.height.equalTo(20)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func toggle() {
|
||||
self.selected = !selected
|
||||
toggleCallback()
|
||||
}
|
||||
|
||||
override func draw(_ dirtyRect: NSRect) {
|
||||
super.draw(dirtyRect)
|
||||
|
||||
let color = selected ? StatusColor.green : NSColor.tertiaryLabelColor
|
||||
let title = selected ? "ON" : "OFF"
|
||||
|
||||
if #available(OSX 10.14, *) {
|
||||
toggleButton.title = title
|
||||
toggleButton.font = NSFont.systemFont(ofSize: 11)
|
||||
toggleButton.contentTintColor = color
|
||||
} else {
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.alignment = .center
|
||||
|
||||
let attributes: [NSAttributedString.Key: Any] = [
|
||||
.font: NSFont.systemFont(ofSize: 11),
|
||||
.foregroundColor: color,
|
||||
.paragraphStyle: paragraphStyle
|
||||
]
|
||||
|
||||
toggleButton.attributedTitle = NSAttributedString(string: title, attributes: attributes)
|
||||
}
|
||||
|
||||
toggleButton.layer?.borderColor = color.cgColor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
//
|
||||
// EditorTableViewController.swift
|
||||
// I2PLauncher
|
||||
//
|
||||
// Created by Mikal Villa on 08/04/2019.
|
||||
// Copyright © 2019 The I2P Project. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import SnapKit
|
||||
|
||||
class EditorTableViewController: NSObject, SwitchableTableViewController {
|
||||
let contentView: NSStackView
|
||||
let scrollView: CustomScrollView
|
||||
let tableView = NSTableView()
|
||||
|
||||
let allServices: [BaseService] = BaseService.all().sorted()
|
||||
var filteredServices: [BaseService]
|
||||
var selectedServices: [BaseService] = []//Preferences.shared().selectedServices
|
||||
|
||||
var selectionChanged = false
|
||||
|
||||
let settingsView = SettingsView()
|
||||
|
||||
var hidden: Bool = true
|
||||
|
||||
init(contentView: NSStackView, scrollView: CustomScrollView) {
|
||||
self.contentView = contentView
|
||||
self.scrollView = scrollView
|
||||
self.filteredServices = allServices
|
||||
|
||||
print(allServices)
|
||||
|
||||
super.init()
|
||||
setup()
|
||||
}
|
||||
|
||||
func setup() {
|
||||
tableView.frame = scrollView.bounds
|
||||
let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: "editorColumnIdentifier"))
|
||||
column.width = 200
|
||||
tableView.addTableColumn(column)
|
||||
tableView.autoresizesSubviews = true
|
||||
tableView.wantsLayer = true
|
||||
tableView.layer?.cornerRadius = 6
|
||||
tableView.headerView = nil
|
||||
tableView.rowHeight = 30
|
||||
tableView.gridStyleMask = NSTableView.GridLineStyle.init(rawValue: 0)
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
tableView.selectionHighlightStyle = .none
|
||||
tableView.backgroundColor = NSColor.clear
|
||||
|
||||
settingsView.isHidden = true
|
||||
settingsView.searchCallback = { [weak self] searchString in
|
||||
guard
|
||||
let strongSelf = self,
|
||||
let allServices = strongSelf.allServices as? [Service]
|
||||
else { return }
|
||||
|
||||
if searchString.trimmingCharacters(in: .whitespacesAndNewlines) == "" {
|
||||
strongSelf.filteredServices = allServices
|
||||
} else {
|
||||
// Can't filter array with NSPredicate without making Service inherit KVO from NSObject, therefore we create
|
||||
// an array of service names that we can run the predicate on
|
||||
let allServiceNames = allServices.compactMap { $0.name } as NSArray
|
||||
let predicate = NSPredicate(format: "SELF LIKE[cd] %@", argumentArray: ["*\(searchString)*"])
|
||||
guard let filteredServiceNames = allServiceNames.filtered(using: predicate) as? [String] else { return }
|
||||
|
||||
strongSelf.filteredServices = allServices.filter { filteredServiceNames.contains($0.name) }
|
||||
}
|
||||
|
||||
strongSelf.tableView.reloadData()
|
||||
}
|
||||
|
||||
contentView.addSubview(settingsView)
|
||||
settingsView.snp.makeConstraints { make in
|
||||
make.top.left.right.equalTo(0)
|
||||
make.height.equalTo(130)
|
||||
}
|
||||
}
|
||||
|
||||
func willShow() {
|
||||
self.selectionChanged = false
|
||||
|
||||
scrollView.topConstraint?.update(offset: settingsView.frame.size.height)
|
||||
scrollView.documentView = tableView
|
||||
|
||||
settingsView.isHidden = false
|
||||
|
||||
// We should be using NSWindow's makeFirstResponder: instead of the search field's selectText:, but in this case, makeFirstResponder
|
||||
// is causing a bug where the search field "gets focused" twice (focus ring animation) the first time it's drawn.
|
||||
settingsView.searchField.selectText(nil)
|
||||
|
||||
resizeViews()
|
||||
}
|
||||
|
||||
func resizeViews() {
|
||||
tableView.frame = scrollView.bounds
|
||||
tableView.tableColumns.first?.width = tableView.frame.size.width
|
||||
|
||||
scrollView.frame.size.height = 400
|
||||
|
||||
(NSApp.delegate as? SwiftApplicationDelegate)?.popupController.resizePopup(
|
||||
height: scrollView.frame.size.height + 30 // bottomBar.frame.size.height
|
||||
)
|
||||
}
|
||||
|
||||
func willOpenPopup() {
|
||||
resizeViews()
|
||||
}
|
||||
|
||||
func didOpenPopup() {
|
||||
settingsView.searchField.window?.makeFirstResponder(settingsView.searchField)
|
||||
}
|
||||
|
||||
func willHide() {
|
||||
settingsView.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
extension EditorTableViewController: NSTableViewDataSource {
|
||||
func numberOfRows(in tableView: NSTableView) -> Int {
|
||||
return filteredServices.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
extension EditorTableViewController: NSTableViewDelegate {
|
||||
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
|
||||
let identifier = tableColumn?.identifier ?? NSUserInterfaceItemIdentifier(rawValue: "identifier")
|
||||
let cell = tableView.makeView(withIdentifier: identifier, owner: self) ?? EditorTableCell()
|
||||
|
||||
guard let view = cell as? EditorTableCell else { return nil }
|
||||
guard let service = filteredServices[row] as? Service else { return nil }
|
||||
|
||||
view.textField?.stringValue = service.name
|
||||
view.selected = selectedServices.contains(service)
|
||||
view.toggleCallback = { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
|
||||
strongSelf.selectionChanged = true
|
||||
|
||||
if view.selected {
|
||||
self?.selectedServices.append(service)
|
||||
} else {
|
||||
if let index = self?.selectedServices.index(of: service) {
|
||||
self?.selectedServices.remove(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
//Preferences.shared().selectedServices = strongSelf.selectedServices
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? {
|
||||
let cellIdentifier = NSUserInterfaceItemIdentifier(rawValue: "rowView")
|
||||
let cell = tableView.makeView(withIdentifier: cellIdentifier, owner: self) ?? ServiceTableRowView()
|
||||
|
||||
guard let view = cell as? ServiceTableRowView else { return nil }
|
||||
|
||||
view.showSeparator = row + 1 < filteredServices.count
|
||||
|
||||
return view
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// SectionHeaderView.swift
|
||||
// I2PLauncher
|
||||
//
|
||||
// Created by Mikal Villa on 09/04/2019.
|
||||
// Copyright © 2019 The I2P Project. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
class SectionHeaderView: NSTextField {
|
||||
init(name: String) {
|
||||
super.init(frame: .zero)
|
||||
|
||||
setup()
|
||||
self.stringValue = name
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(frame: .zero)
|
||||
setup()
|
||||
}
|
||||
|
||||
private func setup() {
|
||||
self.isEditable = false
|
||||
self.isBordered = false
|
||||
self.isSelectable = false
|
||||
let italicFont = NSFontManager.shared.font(withFamily: NSFont.systemFont(ofSize: 10).fontName,
|
||||
traits: NSFontTraitMask.italicFontMask,
|
||||
weight: 5,
|
||||
size: 10)
|
||||
self.font = italicFont
|
||||
self.textColor = NSColor.secondaryLabelColor
|
||||
self.maximumNumberOfLines = 1
|
||||
self.cell!.truncatesLastVisibleLine = true
|
||||
self.backgroundColor = NSColor.clear
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// SettingsView.swift
|
||||
// I2PLauncher
|
||||
//
|
||||
// Created by Mikal Villa on 08/04/2019.
|
||||
// Copyright © 2019 The I2P Project. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import SnapKit
|
||||
|
||||
class SettingsView: NSView {
|
||||
let settingsHeader = SectionHeaderView(name: "Hmm")
|
||||
let notifyCheckbox = NSButton()
|
||||
|
||||
let servicesHeader = SectionHeaderView(name: "Services")
|
||||
let searchField = NSSearchField()
|
||||
|
||||
var searchCallback: ((String) -> Void)?
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
setup()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(frame: .zero)
|
||||
setup()
|
||||
}
|
||||
|
||||
func setup() {
|
||||
addSubview(settingsHeader)
|
||||
addSubview(notifyCheckbox)
|
||||
addSubview(servicesHeader)
|
||||
addSubview(searchField)
|
||||
|
||||
let smallFont = NSFont.systemFont(ofSize: NSFont.systemFontSize(for: .small))
|
||||
|
||||
|
||||
notifyCheckbox.setButtonType(.switch)
|
||||
notifyCheckbox.title = "Notify when a status changes"
|
||||
notifyCheckbox.font = smallFont
|
||||
notifyCheckbox.state = Preferences.shared().notifyOnStatusChange ? .on : .off
|
||||
notifyCheckbox.action = #selector(SettingsView.updateNotifyOnStatusChange)
|
||||
notifyCheckbox.target = self
|
||||
|
||||
searchField.sendsSearchStringImmediately = true
|
||||
searchField.sendsWholeSearchString = false
|
||||
searchField.action = #selector(SettingsView.filterServices)
|
||||
searchField.target = self
|
||||
|
||||
settingsHeader.snp.makeConstraints { make in
|
||||
make.top.left.equalTo(6)
|
||||
make.right.equalTo(-6)
|
||||
make.height.equalTo(16)
|
||||
}
|
||||
|
||||
/*startAtLoginCheckbox.snp.makeConstraints { make in
|
||||
make.top.equalTo(settingsHeader.snp.bottom).offset(6)
|
||||
make.left.equalTo(14)
|
||||
make.right.equalTo(-14)
|
||||
make.height.equalTo(18)
|
||||
}*/
|
||||
|
||||
notifyCheckbox.snp.makeConstraints { make in
|
||||
make.top.equalTo(settingsHeader.snp.bottom).offset(6)
|
||||
make.left.equalTo(14)
|
||||
make.right.equalTo(-14).priority(200)
|
||||
make.height.equalTo(18)
|
||||
}
|
||||
|
||||
servicesHeader.snp.makeConstraints { make in
|
||||
make.top.equalTo(notifyCheckbox.snp.bottom).offset(10)
|
||||
make.left.equalTo(6)
|
||||
make.right.equalTo(-6)
|
||||
make.height.equalTo(16)
|
||||
}
|
||||
|
||||
searchField.snp.makeConstraints { make in
|
||||
make.top.equalTo(servicesHeader.snp.bottom).offset(6)
|
||||
make.left.equalTo(12)
|
||||
make.right.equalTo(-12)
|
||||
make.height.equalTo(22)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc private func updateNotifyOnStatusChange() {
|
||||
Preferences.shared().notifyOnStatusChange = (notifyCheckbox.state == .on)
|
||||
}
|
||||
|
||||
@objc private func filterServices() {
|
||||
searchCallback?(searchField.stringValue)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user