Mac OSX Launcher: EditorTableView for the 2019 redesign/UI improvements.

This commit is contained in:
meeh
2019-05-02 16:14:38 +00:00
parent 20413f00c0
commit 9caa7a61b0
4 changed files with 405 additions and 0 deletions

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

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