From 9caa7a61b0235b5e7e7e66b5f080ee7411c2bd63 Mon Sep 17 00:00:00 2001 From: meeh Date: Thu, 2 May 2019 16:14:38 +0000 Subject: [PATCH] Mac OSX Launcher: EditorTableView for the 2019 redesign/UI improvements. --- .../EditorTableView/EditorTableCell.swift | 99 ++++++++++ .../EditorTableViewController.swift | 171 ++++++++++++++++++ .../EditorTableView/SectionHeaderView.swift | 39 ++++ .../EditorTableView/SettingsView.swift | 96 ++++++++++ 4 files changed, 405 insertions(+) create mode 100644 launchers/macosx/I2PLauncher/userinterface/Style2019/EditorTableView/EditorTableCell.swift create mode 100644 launchers/macosx/I2PLauncher/userinterface/Style2019/EditorTableView/EditorTableViewController.swift create mode 100644 launchers/macosx/I2PLauncher/userinterface/Style2019/EditorTableView/SectionHeaderView.swift create mode 100644 launchers/macosx/I2PLauncher/userinterface/Style2019/EditorTableView/SettingsView.swift diff --git a/launchers/macosx/I2PLauncher/userinterface/Style2019/EditorTableView/EditorTableCell.swift b/launchers/macosx/I2PLauncher/userinterface/Style2019/EditorTableView/EditorTableCell.swift new file mode 100644 index 000000000..23580555d --- /dev/null +++ b/launchers/macosx/I2PLauncher/userinterface/Style2019/EditorTableView/EditorTableCell.swift @@ -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 + } +} + diff --git a/launchers/macosx/I2PLauncher/userinterface/Style2019/EditorTableView/EditorTableViewController.swift b/launchers/macosx/I2PLauncher/userinterface/Style2019/EditorTableView/EditorTableViewController.swift new file mode 100644 index 000000000..3f109304d --- /dev/null +++ b/launchers/macosx/I2PLauncher/userinterface/Style2019/EditorTableView/EditorTableViewController.swift @@ -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 + } +} diff --git a/launchers/macosx/I2PLauncher/userinterface/Style2019/EditorTableView/SectionHeaderView.swift b/launchers/macosx/I2PLauncher/userinterface/Style2019/EditorTableView/SectionHeaderView.swift new file mode 100644 index 000000000..1ef117c71 --- /dev/null +++ b/launchers/macosx/I2PLauncher/userinterface/Style2019/EditorTableView/SectionHeaderView.swift @@ -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 + } +} + diff --git a/launchers/macosx/I2PLauncher/userinterface/Style2019/EditorTableView/SettingsView.swift b/launchers/macosx/I2PLauncher/userinterface/Style2019/EditorTableView/SettingsView.swift new file mode 100644 index 000000000..98d2f2b4b --- /dev/null +++ b/launchers/macosx/I2PLauncher/userinterface/Style2019/EditorTableView/SettingsView.swift @@ -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) + } +} +