Added a programs list.

This commit is contained in:
2025-01-08 15:19:40 -08:00
parent 76713bed2d
commit 8b0e42a7c8
6 changed files with 331 additions and 102 deletions

View File

@@ -7,10 +7,19 @@ fileprivate let logger = Logger(
category: String("Helpers") category: String("Helpers")
) )
enum ViewConstants {
static let spacing2: CGFloat = 2
static let spacing5: CGFloat = 2
static let spacing10: CGFloat = 10
static let spacing20: CGFloat = 20
static let spacing40: CGFloat = 40
}
struct Program { struct Program {
let path: String let path: String
let name: String let name: String
let ext: String let ext: String
var img: NSImage?
} }
func keyName(virtualKeyCode: UInt16) -> String? { func keyName(virtualKeyCode: UInt16) -> String? {

View File

@@ -11,7 +11,7 @@ SRCMODULES = Helpers.swift EditableNSTextField.swift EventMonitor.swift \
GlobalEventTap.swift PopoverPanel.swift SearchViewController.swift \ GlobalEventTap.swift PopoverPanel.swift SearchViewController.swift \
SettingsViewController.swift HotKeyManager.swift \ SettingsViewController.swift HotKeyManager.swift \
KeyDetectorButton.swift PathManager.swift MyTableCellView.swift \ KeyDetectorButton.swift PathManager.swift MyTableCellView.swift \
AppDelegate.swift main.swift ProgramTableViewCell.swift AppDelegate.swift main.swift
ARMOBJMODULES = $(addprefix ./arm64/,$(SRCMODULES:.swift=.o)) ARMOBJMODULES = $(addprefix ./arm64/,$(SRCMODULES:.swift=.o))
X86OBJMODULES = $(addprefix ./x86_64/,$(SRCMODULES:.swift=.o)) X86OBJMODULES = $(addprefix ./x86_64/,$(SRCMODULES:.swift=.o))
@@ -27,19 +27,21 @@ FRAMEWORKS = -framework AppKit -framework ServiceManagement
# generates same exact executable, timestamps do change. # generates same exact executable, timestamps do change.
./arm64/%.o: %.swift ./arm64/%.o: %.swift
swift -frontend -c -target arm64-apple-macos$(MACOS_VERSION) $(FLAGS) \ swift -frontend -c -target arm64-apple-macos$(MACOS_VERSION) $(FLAGS) \
-primary-file $< $(filter-out $<, $(SRCMODULES)) $(LIBS) $(FRAMEWORKS) -sdk $(SDK) \ -primary-file $< $(filter-out $<, $(SRCMODULES)) $(LIBS) \
-module-name $(EXEC) -o $@ -emit-module && touch $@ $(FRAMEWORKS) -sdk $(SDK) -module-name $(EXEC) -o $@ \
-emit-module && touch $@
ifdef UNIVERSAL ifdef UNIVERSAL
./x86_64/%.o: %.swift ./x86_64/%.o: %.swift
@swift -frontend -c -target x86_64-apple-macos$(MACOS_VERSION) \ @swift -frontend -c -target x86_64-apple-macos$(MACOS_VERSION) \
$(FLAGS) -primary-file $< $(filter-out $<, $(SRCMODULES)) $(LIBS) $(FRAMEWORKS) \ $(FLAGS) -primary-file $< $(filter-out $<, $(SRCMODULES)) \
-sdk $(SDK) -module-name $(EXEC) -o $@ -emit-module && touch $@ $(LIBS) $(FRAMEWORKS) -sdk $(SDK) -module-name $(EXEC) -o $@ \
-emit-module && touch $@
endif endif
./arm64/$(EXEC): $(ARMOBJMODULES) ./arm64/$(EXEC): $(ARMOBJMODULES)
@ld -syslibroot $(SDK) -lSystem $(FRAMEWORKS) -arch arm64 -macos_version_min \ @ld -syslibroot $(SDK) -lSystem $(FRAMEWORKS) -arch arm64 \
$(MACOS_VERSION).0 \ -macos_version_min $(MACOS_VERSION).0 \
/Library/Developer/CommandLineTools/usr/lib/swift/macosx/libswiftCompatibilityPacks.a \ /Library/Developer/CommandLineTools/usr/lib/swift/macosx/libswiftCompatibilityPacks.a \
-sectcreate __TEXT __info_plist Info.plist \ -sectcreate __TEXT __info_plist Info.plist \
-L /Library/Developer/CommandLineTools/usr/lib/swift/macosx -L \ -L /Library/Developer/CommandLineTools/usr/lib/swift/macosx -L \
@@ -51,8 +53,8 @@ endif
ifdef UNIVERSAL ifdef UNIVERSAL
./x86_64/$(EXEC): $(X86OBJMODULES) ./x86_64/$(EXEC): $(X86OBJMODULES)
@ld -syslibroot $(SDK) -lSystem $(FRAMEWORKS) -arch x86_64 -macos_version_min \ @ld -syslibroot $(SDK) -lSystem $(FRAMEWORKS) -arch x86_64 \
$(MACOS_VERSION).0 \ -macos_version_min $(MACOS_VERSION).0 \
/Library/Developer/CommandLineTools/usr/lib/swift/macosx/libswiftCompatibilityPacks.a \ /Library/Developer/CommandLineTools/usr/lib/swift/macosx/libswiftCompatibilityPacks.a \
-sectcreate __TEXT __info_plist Info.plist \ -sectcreate __TEXT __info_plist Info.plist \
-L /Library/Developer/CommandLineTools/usr/lib/swift/macosx -L \ -L /Library/Developer/CommandLineTools/usr/lib/swift/macosx -L \

View File

@@ -69,11 +69,13 @@ final class PathManager {
atPath: path) atPath: path)
for item in items { for item in items {
let name = String(item.dropLast(4)) let name = String(item.dropLast(4))
if item.hasSuffix(".app") { if item.hasSuffix(".app") {
if !programs.contains(where: { name == $0.name }) { if !programs.contains(where: { name == $0.name }) {
programs.append( programs.append(
Program( Program(
path: path, name: name, ext: ".app")) path: path, name: name, ext: ".app",
img: nil))
} }
} }
} }

View File

@@ -0,0 +1,78 @@
import AppKit
class ProgramTableRowView: NSTableRowView {
override func drawSelection(in dirtyRect: NSRect) {
if self.selectionHighlightStyle != .none {
let selectionColor = NSColor.systemBlue
selectionColor.setFill()
self.bounds.fill()
}
}
}
class ProgramTableViewCell: NSTableCellView {
var id: Int = -1
private(set) var isEditing = false
public var appIconImage: NSImageView = {
let image = NSImageView()
image.image =
NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath)
image.imageScaling = .scaleAxesIndependently
image.translatesAutoresizingMaskIntoConstraints = false
return image
}()
public var titleField: NSTextField = {
let field = NSTextField(labelWithString: "")
field.lineBreakMode = .byTruncatingTail
field.translatesAutoresizingMaskIntoConstraints = false
return field
}()
public var progPathLabel: NSTextField = {
let textField = NSTextField(labelWithString: "")
textField.cell?.lineBreakMode = .byTruncatingTail
textField.font = NSFont.systemFont(
ofSize: NSFontDescriptor.preferredFontDescriptor(
forTextStyle: .caption1).pointSize, weight: .medium)
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
addSubview(appIconImage)
addSubview(titleField)
addSubview(progPathLabel)
NSLayoutConstraint.activate([
appIconImage.widthAnchor.constraint(equalToConstant: 40),
appIconImage.heightAnchor.constraint(equalToConstant: 40),
appIconImage.topAnchor.constraint(equalTo: topAnchor),
appIconImage.bottomAnchor.constraint(equalTo: bottomAnchor),
appIconImage.leadingAnchor.constraint(equalTo: leadingAnchor),
titleField.topAnchor.constraint(
equalTo: appIconImage.topAnchor,
constant: ViewConstants.spacing2),
titleField.leadingAnchor.constraint(
equalTo: appIconImage.trailingAnchor,
constant: ViewConstants.spacing5),
titleField.trailingAnchor.constraint(equalTo: trailingAnchor),
progPathLabel.topAnchor.constraint(
equalTo: titleField.bottomAnchor),
progPathLabel.leadingAnchor.constraint(
equalTo: titleField.leadingAnchor),
progPathLabel.trailingAnchor.constraint(
equalTo: titleField.trailingAnchor),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@@ -1,22 +1,20 @@
import AppKit import AppKit
import OSLog import OSLog
fileprivate enum ViewConstants {
static let spacing2: CGFloat = 2
static let spacing10: CGFloat = 10
static let spacing20: CGFloat = 20
static let spacing40: CGFloat = 40
}
class SearchViewController: NSViewController, NSTextFieldDelegate, class SearchViewController: NSViewController, NSTextFieldDelegate,
NSPopoverDelegate NSPopoverDelegate, NSTableViewDataSource, NSTableViewDelegate
{ {
fileprivate static let logger = Logger( fileprivate static let logger = Logger(
subsystem: Bundle.main.bundleIdentifier!, subsystem: Bundle.main.bundleIdentifier!,
category: String(describing: SearchViewController.self) category: String(describing: SearchViewController.self)
) )
var foundProgram: Program? = nil private var keyboardEvents: EventMonitor?
private var foundProgram: Program? = nil
private var programsList: [Program] = []
private var programsTableViewSelection = 0
private var settingsPopover: NSPopover = { private var settingsPopover: NSPopover = {
let popover = NSPopover() let popover = NSPopover()
@@ -37,21 +35,11 @@ class SearchViewController: NSViewController, NSTextFieldDelegate,
private var searchInput: EditableNSTextField = { private var searchInput: EditableNSTextField = {
let textField = EditableNSTextField() let textField = EditableNSTextField()
textField.placeholderString = "Search programs . . ." textField.placeholderString = "Search programs . . ."
textField.usesSingleLineMode = false
textField.bezelStyle = .roundedBezel textField.bezelStyle = .roundedBezel
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
private var programsLabel: NSTextField = {
let textField = NSTextField()
textField.stringValue = ""
textField.isEditable = false
textField.isBezeled = false
textField.drawsBackground = false
textField.alignment = .left
textField.font = NSFont.systemFont( textField.font = NSFont.systemFont(
ofSize: NSFontDescriptor.preferredFontDescriptor( ofSize: NSFontDescriptor.preferredFontDescriptor(
forTextStyle: .body).pointSize, weight: .bold) forTextStyle: .title3).pointSize, weight: .medium)
textField.translatesAutoresizingMaskIntoConstraints = false textField.translatesAutoresizingMaskIntoConstraints = false
return textField return textField
}() }()
@@ -68,31 +56,70 @@ class SearchViewController: NSViewController, NSTextFieldDelegate,
return button return button
}() }()
private var tableScrollView: NSScrollView = {
let scroll = NSScrollView()
scroll.contentInsets = NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
scroll.drawsBackground = false
scroll.translatesAutoresizingMaskIntoConstraints = false
return scroll
}()
private var programsTableView: MyNSTableView = {
let table = MyNSTableView()
table.style = NSTableView.Style.plain
table.backgroundColor = .clear
table.usesAutomaticRowHeights = true
table.headerView = nil
table.allowsMultipleSelection = false
table.allowsColumnReordering = false
table.allowsColumnResizing = false
table.allowsColumnSelection = false
table.addTableColumn(NSTableColumn(
identifier: NSUserInterfaceItemIdentifier("Program")))
table.doubleAction = #selector(tableDoubleClick)
table.translatesAutoresizingMaskIntoConstraints = false
return table
}()
private func addSubviews() { private func addSubviews() {
view.addSubview(appIconImage) view.addSubview(appIconImage)
view.addSubview(searchInput) view.addSubview(searchInput)
view.addSubview(programsLabel)
view.addSubview(settingsButton) view.addSubview(settingsButton)
view.addSubview(tableScrollView)
} }
var viewBottomAnchorTable: NSLayoutConstraint?
var viewBottomAnchorImage: NSLayoutConstraint?
private func setConstraints() { private func setConstraints() {
viewBottomAnchorTable = tableScrollView.bottomAnchor.constraint(
equalTo: view.bottomAnchor,
constant: -ViewConstants.spacing10)
viewBottomAnchorImage = appIconImage.bottomAnchor.constraint(
equalTo: view.bottomAnchor,
constant: -ViewConstants.spacing10)
viewBottomAnchorTable?.isActive = false
viewBottomAnchorImage?.isActive = true
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
appIconImage.widthAnchor.constraint(equalToConstant: 70), appIconImage.widthAnchor.constraint(equalToConstant: 60),
appIconImage.heightAnchor.constraint( appIconImage.heightAnchor.constraint(
equalTo: appIconImage.widthAnchor, multiplier: 1), equalTo: appIconImage.widthAnchor, multiplier: 1),
appIconImage.topAnchor.constraint(equalTo: view.topAnchor, appIconImage.topAnchor.constraint(equalTo: view.topAnchor,
constant: ViewConstants.spacing20), constant: ViewConstants.spacing10),
appIconImage.bottomAnchor.constraint(
equalTo: view.bottomAnchor,
constant: -ViewConstants.spacing10),
appIconImage.leadingAnchor.constraint( appIconImage.leadingAnchor.constraint(
equalTo: view.leadingAnchor, equalTo: view.leadingAnchor,
constant: ViewConstants.spacing10), constant: ViewConstants.spacing10),
searchInput.widthAnchor.constraint(equalToConstant: 300), searchInput.widthAnchor.constraint(equalToConstant: 300),
searchInput.topAnchor.constraint( searchInput.centerYAnchor.constraint(
equalTo: appIconImage.topAnchor), equalTo: appIconImage.centerYAnchor),
searchInput.leadingAnchor.constraint( searchInput.leadingAnchor.constraint(
equalTo: appIconImage.trailingAnchor, equalTo: appIconImage.trailingAnchor,
constant: ViewConstants.spacing10), constant: ViewConstants.spacing10),
@@ -106,24 +133,74 @@ class SearchViewController: NSViewController, NSTextFieldDelegate,
equalTo: view.trailingAnchor, equalTo: view.trailingAnchor,
constant: -ViewConstants.spacing10), constant: -ViewConstants.spacing10),
programsLabel.topAnchor.constraint( tableScrollView.heightAnchor.constraint(equalToConstant: 210),
equalTo: searchInput.bottomAnchor, tableScrollView.topAnchor.constraint(
equalTo: appIconImage.bottomAnchor,
constant: ViewConstants.spacing10), constant: ViewConstants.spacing10),
programsLabel.leadingAnchor.constraint( tableScrollView.leadingAnchor.constraint(
equalTo: appIconImage.trailingAnchor, equalTo: view.leadingAnchor),
constant: ViewConstants.spacing10), tableScrollView.trailingAnchor.constraint(
programsLabel.trailingAnchor.constraint( equalTo: view.trailingAnchor)
equalTo: searchInput.trailingAnchor),
]) ])
} }
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
keyboardEvents = LocalEventMonitor(mask: [.keyDown], handler:
{ [weak self] event in
let key = event.keyCode
let modifiers = event.modifierFlags.rawValue
let command = NSEvent.ModifierFlags.command.rawValue
let shift = NSEvent.ModifierFlags.shift.rawValue
let control = NSEvent.ModifierFlags.control.rawValue
let option = NSEvent.ModifierFlags.option.rawValue
// TODO: Implement helper functions for modifiers.
if let controller = self {
if ((modifiers & control) == control &&
(modifiers & (command | shift | option)) == 0 &&
key == 35) || // P
(modifiers & (command | control | shift | option)) == 0 &&
(key == 126) // UP
{
controller.programsTableViewSelection -= 1
} else if ((modifiers & control) == control &&
(modifiers & (command | shift | option)) == 0 &&
key == 45) || // N
(modifiers & (command | control | shift | option)) == 0 &&
(key == 125) // DOWN
{
controller.programsTableViewSelection += 1
}
if controller.programsTableViewSelection >
controller.programsList.count-1
{
controller.programsTableViewSelection =
controller.programsList.count-1
} else if controller.programsTableViewSelection < 0 {
controller.programsTableViewSelection = 0
}
let select = controller.programsTableViewSelection
self?.programsTableView.selectRowIndexes(
IndexSet(integer: select),
byExtendingSelection: false)
self?.programsTableView.scrollRowToVisible(select)
}
return event
})
settingsPopover.delegate = self settingsPopover.delegate = self
searchInput.delegate = self searchInput.delegate = self
tableScrollView.documentView = programsTableView
programsTableView.dataSource = self
programsTableView.delegate = self
addSubviews() addSubviews()
setConstraints() setConstraints()
} }
@@ -131,17 +208,37 @@ class SearchViewController: NSViewController, NSTextFieldDelegate,
override func viewDidAppear() { override func viewDidAppear() {
super.viewDidAppear() super.viewDidAppear()
self.view.window?.center() keyboardEvents?.start()
view.window?.center()
view.window?.makeFirstResponder(searchInput)
// searchInput should select all text whenever window appears. // searchInput should select all text whenever window appears.
NSApp.sendAction(#selector(NSResponder.selectAll(_:)), NSApp.sendAction(#selector(NSResponder.selectAll(_:)),
to: nil, from: self) to: nil, from: self)
} }
override func viewDidDisappear() {
super.viewDidDisappear()
keyboardEvents?.stop()
}
override func loadView() { override func loadView() {
self.view = NSView() self.view = NSView()
} }
private func reloadProgramsTableViewData() {
if programsList.count > 0 {
viewBottomAnchorTable?.isActive = true
viewBottomAnchorImage?.isActive = false
} else {
viewBottomAnchorTable?.isActive = false
viewBottomAnchorImage?.isActive = true
}
programsTableView.reloadData()
}
@objc @objc
func openSettings() { func openSettings() {
// HACK: This is an interseting behavior. When NSPopover appears // HACK: This is an interseting behavior. When NSPopover appears
@@ -154,48 +251,13 @@ class SearchViewController: NSViewController, NSTextFieldDelegate,
of: settingsButton, preferredEdge: .maxY) of: settingsButton, preferredEdge: .maxY)
} }
func controlTextDidChange(_ obj: Notification) { @objc
guard let searchInput = obj.object as? EditableNSTextField private func tableDoubleClick() {
else { return } let program = programsList[programsTableView.clickedRow]
openProgram(program)
var list = ""
let programs = PathManager.shared.programs
for program in programs {
if program.name.lowercased().contains(
searchInput.stringValue.lowercased())
{
if !list.isEmpty {
list += ", "
}
list += program.name + program.ext
foundProgram = program
break
} else {
foundProgram = nil
}
} }
if let program = foundProgram { private func openProgram(_ program: Program) {
programsLabel.stringValue =
program.name + program.ext + " (\(program.path))"
let url = URL(fileURLWithPath: program.path)
.appendingPathComponent(program.name+program.ext)
appIconImage.image = NSWorkspace.shared.icon(forFile: url.path)
} else {
programsLabel.stringValue = ""
appIconImage.image =
NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath)
}
}
func control(_ control: NSControl, textView: NSTextView,
doCommandBy commandSelector: Selector) -> Bool
{
if commandSelector == #selector(NSResponder.insertNewline(_:)) {
if let program = foundProgram {
let url = URL(fileURLWithPath: program.path) let url = URL(fileURLWithPath: program.path)
.appendingPathComponent(program.name+program.ext) .appendingPathComponent(program.name+program.ext)
let config = NSWorkspace.OpenConfiguration() let config = NSWorkspace.OpenConfiguration()
@@ -207,6 +269,8 @@ class SearchViewController: NSViewController, NSTextFieldDelegate,
Self.logger.debug("\(error.localizedDescription)") Self.logger.debug("\(error.localizedDescription)")
} else { } else {
Self.logger.debug("Program opened successfully") Self.logger.debug("Program opened successfully")
// NOTE: This needs a window! Do not just copy-paste
// this block elsewhere.
DispatchQueue.main.async { DispatchQueue.main.async {
if let window = self?.view.window { if let window = self?.view.window {
window.resignKey() window.resignKey()
@@ -215,12 +279,65 @@ class SearchViewController: NSViewController, NSTextFieldDelegate,
} }
} }
} }
func controlTextDidChange(_ obj: Notification) {
guard let searchInput = obj.object as? EditableNSTextField
else { return }
let programs = PathManager.shared.programs
programsList = []
for i in programs.indices {
var program = programs[i]
if programsList.count >= 10 {
break
}
if program.name.lowercased().contains(
searchInput.stringValue.lowercased())
{
let url = URL(fileURLWithPath: program.path)
.appendingPathComponent(program.name+program.ext)
let image = NSWorkspace.shared.icon(forFile: url.path)
program.img = image
programsList.append(program)
}
}
reloadProgramsTableViewData()
programsTableViewSelection = 0
programsTableView.selectRowIndexes(
IndexSet(integer: programsTableViewSelection),
byExtendingSelection: false)
programsTableView.scrollRowToVisible(programsTableViewSelection)
if programsList.count > 0 {
let program = programsList[0]
let url = URL(fileURLWithPath: program.path)
.appendingPathComponent(program.name+program.ext)
appIconImage.image = NSWorkspace.shared.icon(forFile: url.path)
} else {
appIconImage.image =
NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath)
}
}
func control(_ control: NSControl, textView: NSTextView,
doCommandBy commandSelector: Selector) -> Bool
{
if commandSelector == #selector(NSResponder.insertNewline(_:)) {
let program = programsList[programsTableViewSelection]
openProgram(program)
NSApp.sendAction(#selector(NSResponder.selectAll(_:)), NSApp.sendAction(#selector(NSResponder.selectAll(_:)),
to: nil, from: self) to: nil, from: self)
return true return true
} else if commandSelector == #selector(NSResponder.insertTab(_:)) { } else if commandSelector == #selector(NSResponder.insertTab(_:)) {
return true return true
} else if commandSelector == #selector(NSResponder.moveUp(_:)) ||
commandSelector == #selector(NSResponder.moveDown(_:))
{
// Ignore arrows keys up or down because we use those to
// navigate the programs list.
return true
} }
return false return false
@@ -233,4 +350,35 @@ class SearchViewController: NSViewController, NSTextFieldDelegate,
func popoverWillClose(_ notification: Notification) { func popoverWillClose(_ notification: Notification) {
searchInput.becomeFirstResponder() searchInput.becomeFirstResponder()
} }
func numberOfRows(in tableView: NSTableView) -> Int {
return programsList.count
}
func tableView(_ tableView: NSTableView,
rowViewForRow row: Int) -> NSTableRowView?
{
return ProgramTableRowView()
}
func tableView(_ tableView: NSTableView,
viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?
{
let cell = ProgramTableViewCell()
let program = programsList[row]
// PERF: This is very slow, even with 10 items on the list! It has
// to be the image of concern. UIKit has reusable cells,
// is that possible? Or is fetching an image is slow?
cell.titleField.stringValue = program.name + program.ext
cell.progPathLabel.stringValue = program.path
cell.appIconImage.image = program.img
cell.id = row
return cell
}
}
final class MyNSTableView: NSTableView {
override var acceptsFirstResponder: Bool { false }
} }

View File

@@ -3,14 +3,6 @@ import Carbon
import ServiceManagement import ServiceManagement
import OSLog import OSLog
fileprivate enum ViewConstants {
static let spacing2: CGFloat = 2
static let spacing5: CGFloat = 2
static let spacing10: CGFloat = 10
static let spacing20: CGFloat = 20
static let spacing40: CGFloat = 40
}
class SettingsViewController: NSViewController, NSTextFieldDelegate, class SettingsViewController: NSViewController, NSTextFieldDelegate,
KeyDetectorButtonDelegate, NSTableViewDataSource, NSTableViewDelegate, KeyDetectorButtonDelegate, NSTableViewDataSource, NSTableViewDelegate,
MyTableCellViewDelegate MyTableCellViewDelegate
@@ -321,8 +313,6 @@ class SettingsViewController: NSViewController, NSTextFieldDelegate,
pathsTableView.dataSource = self pathsTableView.dataSource = self
pathsTableView.delegate = self pathsTableView.delegate = self
pathsTableView.delegate = self
pathsControl.target = self pathsControl.target = self
pathsControl.action = #selector(affectPaths(_:)) pathsControl.action = #selector(affectPaths(_:))