675 lines
22 KiB
Swift
675 lines
22 KiB
Swift
import AppKit
|
|
import Carbon
|
|
import ServiceManagement
|
|
|
|
// TODO: Rework the paths table and selection. Right now, it is very
|
|
// disfunctional and error-prone.
|
|
class SettingsViewController: NSViewController,
|
|
NSTextFieldDelegate, KeyDetectorButtonDelegate,
|
|
NSTableViewDataSource, NSTableViewDelegate,
|
|
PathsTableCellViewDelegate
|
|
{
|
|
private var keyboardEvents: EventMonitor?
|
|
|
|
private var recording = false
|
|
|
|
// NOTE: This is the default shortcut. If you were to change it, don't
|
|
// forget to change other places in this file and delegate, too.
|
|
private var keyCode = Int(kVK_Space)
|
|
private var modifiers = Int(optionKey)
|
|
|
|
private var paths: [String] = []
|
|
|
|
private var dirPicker: NSOpenPanel?
|
|
|
|
private var shortcutsLabel: NSTextField = {
|
|
let textField = NSTextField(labelWithString: "Shortcut")
|
|
textField.font =
|
|
NSFont.systemFont(ofSize: NSFontDescriptor
|
|
.preferredFontDescriptor(forTextStyle: .title2).pointSize,
|
|
weight: .bold)
|
|
textField.translatesAutoresizingMaskIntoConstraints = false
|
|
return textField
|
|
}()
|
|
|
|
private var aboutButton: NSButton = {
|
|
let button = NSButton()
|
|
button.image = systemImage("info.circle.fill", .title2, .large,
|
|
.init(paletteColors: [.white, .systemGray]))
|
|
button.isBordered = false
|
|
button.action = #selector(showAbout)
|
|
button.sizeToFit()
|
|
button.toolTip = "About"
|
|
button.translatesAutoresizingMaskIntoConstraints = false
|
|
return button
|
|
}()
|
|
|
|
private var ctrlButton: NSButton = {
|
|
let button = NSButton()
|
|
button.title = "⌃"
|
|
button.action = #selector(handleModifiers)
|
|
button.setButtonType(.pushOnPushOff)
|
|
button.sizeToFit()
|
|
button.bezelStyle = .rounded
|
|
button.translatesAutoresizingMaskIntoConstraints = false
|
|
return button
|
|
}()
|
|
|
|
private var cmdButton: NSButton = {
|
|
let button = NSButton()
|
|
button.title = "⌘"
|
|
button.action = #selector(handleModifiers)
|
|
button.setButtonType(.pushOnPushOff)
|
|
button.sizeToFit()
|
|
button.bezelStyle = .rounded
|
|
button.translatesAutoresizingMaskIntoConstraints = false
|
|
return button
|
|
}()
|
|
|
|
private var optButton: NSButton = {
|
|
let button = NSButton()
|
|
button.title = "⌥"
|
|
button.action = #selector(handleModifiers)
|
|
button.setButtonType(.pushOnPushOff)
|
|
button.sizeToFit()
|
|
button.bezelStyle = .rounded
|
|
button.translatesAutoresizingMaskIntoConstraints = false
|
|
return button
|
|
}()
|
|
|
|
private var shiftButton: NSButton = {
|
|
let button = NSButton()
|
|
button.title = "⇧"
|
|
button.action = #selector(handleModifiers)
|
|
button.setButtonType(.pushOnPushOff)
|
|
button.sizeToFit()
|
|
button.bezelStyle = .rounded
|
|
button.translatesAutoresizingMaskIntoConstraints = false
|
|
return button
|
|
}()
|
|
|
|
private var plusLabel: NSTextField = {
|
|
let textField = NSTextField()
|
|
textField.stringValue = "+"
|
|
textField.isEditable = false
|
|
textField.isBezeled = false
|
|
textField.drawsBackground = false
|
|
textField.alignment = .center
|
|
textField.font = NSFont
|
|
.systemFont(ofSize: NSFontDescriptor
|
|
.preferredFontDescriptor(forTextStyle: .body).pointSize,
|
|
weight: .bold)
|
|
textField.translatesAutoresizingMaskIntoConstraints = false
|
|
return textField
|
|
}()
|
|
|
|
private var recordButton: KeyDetectorButton = {
|
|
let button = KeyDetectorButton()
|
|
button.title = "Record"
|
|
button.sizeToFit()
|
|
button.bezelStyle = .rounded
|
|
button.translatesAutoresizingMaskIntoConstraints = false
|
|
return button
|
|
}()
|
|
|
|
private var pathsLabel: NSTextField = {
|
|
let textField =
|
|
NSTextField(labelWithString: "Application Directories")
|
|
textField.font = NSFont
|
|
.systemFont(ofSize: NSFontDescriptor
|
|
.preferredFontDescriptor(forTextStyle: .title2).pointSize,
|
|
weight: .bold)
|
|
textField.translatesAutoresizingMaskIntoConstraints = false
|
|
return textField
|
|
}()
|
|
|
|
private var tableScrollView: NSScrollView = {
|
|
let scroll = NSScrollView()
|
|
scroll.drawsBackground = false
|
|
scroll.translatesAutoresizingMaskIntoConstraints = false
|
|
return scroll
|
|
}()
|
|
|
|
private var pathsTableView: NSTableView = {
|
|
let table = NSTableView()
|
|
|
|
table.backgroundColor = .clear
|
|
|
|
table.doubleAction = #selector(editItem)
|
|
|
|
table.headerView = nil
|
|
table.allowsMultipleSelection = false
|
|
table.allowsColumnReordering = false
|
|
table.allowsColumnResizing = false
|
|
table.allowsColumnSelection = false
|
|
table.addTableColumn(
|
|
NSTableColumn(
|
|
identifier: NSUserInterfaceItemIdentifier("Paths")
|
|
)
|
|
)
|
|
|
|
table.translatesAutoresizingMaskIntoConstraints = false
|
|
return table
|
|
}()
|
|
|
|
private var pathsControl: NSSegmentedControl = {
|
|
let control = NSSegmentedControl()
|
|
control.segmentCount = 2
|
|
control.segmentStyle = .roundRect
|
|
|
|
control.setImage(NSImage(systemSymbolName: "plus",
|
|
accessibilityDescription: nil), forSegment: 0)
|
|
control.setImage(NSImage(systemSymbolName: "minus",
|
|
accessibilityDescription: nil), forSegment: 1)
|
|
|
|
control.setToolTip("Add Path", forSegment: 0)
|
|
control.setToolTip("Remove Path", forSegment: 1)
|
|
control.trackingMode = .momentary
|
|
|
|
control.translatesAutoresizingMaskIntoConstraints = false
|
|
return control
|
|
}()
|
|
|
|
private var launchAtLoginLabel: NSTextField = {
|
|
let textField = NSTextField(labelWithString: "Launch at login")
|
|
textField.translatesAutoresizingMaskIntoConstraints = false
|
|
return textField
|
|
}()
|
|
|
|
private var launchAtLoginToggle: NSSegmentedControl = {
|
|
let control = NSSegmentedControl()
|
|
control.segmentCount = 2
|
|
control.segmentStyle = .roundRect
|
|
|
|
control.setLabel("Off", forSegment: 0)
|
|
control.setLabel("On", forSegment: 1)
|
|
|
|
control.setToolTip("Off", forSegment: 0)
|
|
control.setToolTip("On", forSegment: 1)
|
|
|
|
control.translatesAutoresizingMaskIntoConstraints = false
|
|
return control
|
|
}()
|
|
|
|
|
|
private var resetAllButton: NSButton = {
|
|
let button = NSButton()
|
|
button.title = "Reset"
|
|
button.action = #selector(reset)
|
|
button.setButtonType(.momentaryLight)
|
|
button.sizeToFit()
|
|
button.bezelStyle = .rounded
|
|
button.isBordered = false
|
|
button.translatesAutoresizingMaskIntoConstraints = false
|
|
return button
|
|
}()
|
|
|
|
private func addSubviews() {
|
|
view.addSubview(shortcutsLabel)
|
|
view.addSubview(aboutButton)
|
|
view.addSubview(ctrlButton)
|
|
view.addSubview(cmdButton)
|
|
view.addSubview(optButton)
|
|
view.addSubview(shiftButton)
|
|
view.addSubview(plusLabel)
|
|
view.addSubview(recordButton)
|
|
|
|
view.addSubview(pathsLabel)
|
|
view.addSubview(tableScrollView)
|
|
view.addSubview(pathsControl)
|
|
|
|
view.addSubview(launchAtLoginLabel)
|
|
view.addSubview(launchAtLoginToggle)
|
|
view.addSubview(resetAllButton)
|
|
}
|
|
|
|
private func setConstraints() {
|
|
NSLayoutConstraint.activate([
|
|
shortcutsLabel.topAnchor
|
|
.constraint(equalTo: view.topAnchor,
|
|
constant: ViewConstants.spacing10),
|
|
shortcutsLabel.leadingAnchor
|
|
.constraint(equalTo: view.leadingAnchor,
|
|
constant: ViewConstants.spacing10),
|
|
|
|
aboutButton.firstBaselineAnchor
|
|
.constraint(equalTo: shortcutsLabel.firstBaselineAnchor),
|
|
aboutButton.trailingAnchor
|
|
.constraint(equalTo: view.trailingAnchor,
|
|
constant: -ViewConstants.spacing10),
|
|
|
|
ctrlButton.topAnchor
|
|
.constraint(equalTo: shortcutsLabel.bottomAnchor,
|
|
constant: ViewConstants.spacing10),
|
|
ctrlButton.leadingAnchor
|
|
.constraint(equalTo: shortcutsLabel.leadingAnchor),
|
|
|
|
cmdButton.centerYAnchor
|
|
.constraint(equalTo: ctrlButton.centerYAnchor),
|
|
cmdButton.leadingAnchor
|
|
.constraint(equalTo: ctrlButton.trailingAnchor,
|
|
constant: ViewConstants.spacing5),
|
|
|
|
optButton.centerYAnchor
|
|
.constraint(equalTo: ctrlButton.centerYAnchor),
|
|
optButton.leadingAnchor
|
|
.constraint(equalTo: cmdButton.trailingAnchor,
|
|
constant: ViewConstants.spacing5),
|
|
|
|
shiftButton.centerYAnchor
|
|
.constraint(equalTo: ctrlButton.centerYAnchor),
|
|
shiftButton.leadingAnchor
|
|
.constraint(equalTo: optButton.trailingAnchor,
|
|
constant: ViewConstants.spacing5),
|
|
|
|
plusLabel.centerYAnchor
|
|
.constraint(equalTo: ctrlButton.centerYAnchor),
|
|
plusLabel.leadingAnchor
|
|
.constraint(equalTo: shiftButton.trailingAnchor,
|
|
constant: ViewConstants.spacing5),
|
|
|
|
recordButton.widthAnchor.constraint(equalToConstant: 40),
|
|
recordButton.centerYAnchor
|
|
.constraint(equalTo: ctrlButton.centerYAnchor),
|
|
recordButton.leadingAnchor
|
|
.constraint(equalTo: plusLabel.trailingAnchor,
|
|
constant: ViewConstants.spacing5),
|
|
|
|
pathsLabel.topAnchor
|
|
.constraint(equalTo: ctrlButton.bottomAnchor,
|
|
constant: ViewConstants.spacing20),
|
|
pathsLabel.leadingAnchor
|
|
.constraint(equalTo: shortcutsLabel.leadingAnchor),
|
|
|
|
tableScrollView.widthAnchor.constraint(equalToConstant: 500),
|
|
tableScrollView.heightAnchor.constraint(equalToConstant: 200),
|
|
tableScrollView.topAnchor
|
|
.constraint(equalTo: pathsLabel.bottomAnchor),
|
|
tableScrollView.leadingAnchor
|
|
.constraint(equalTo: view.leadingAnchor),
|
|
tableScrollView.trailingAnchor
|
|
.constraint(equalTo: view.trailingAnchor),
|
|
|
|
pathsControl.topAnchor
|
|
.constraint(equalTo: tableScrollView.bottomAnchor,
|
|
constant: ViewConstants.spacing10),
|
|
pathsControl.leadingAnchor
|
|
.constraint(equalTo: view.leadingAnchor,
|
|
constant: ViewConstants.spacing10),
|
|
|
|
launchAtLoginLabel.topAnchor
|
|
.constraint(equalTo: pathsControl.bottomAnchor,
|
|
constant: ViewConstants.spacing10),
|
|
launchAtLoginLabel.trailingAnchor
|
|
.constraint(equalTo: launchAtLoginToggle.leadingAnchor,
|
|
constant: -ViewConstants.spacing10),
|
|
|
|
launchAtLoginToggle.firstBaselineAnchor
|
|
.constraint(equalTo: launchAtLoginLabel.firstBaselineAnchor),
|
|
launchAtLoginToggle.trailingAnchor
|
|
.constraint(equalTo: resetAllButton.leadingAnchor,
|
|
constant: -ViewConstants.spacing15),
|
|
|
|
resetAllButton.firstBaselineAnchor
|
|
.constraint(equalTo: launchAtLoginLabel.firstBaselineAnchor),
|
|
resetAllButton.trailingAnchor
|
|
.constraint(equalTo: view.trailingAnchor,
|
|
constant: -ViewConstants.spacing10),
|
|
resetAllButton.bottomAnchor
|
|
.constraint(equalTo: view.bottomAnchor,
|
|
constant: -ViewConstants.spacing10),
|
|
])
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
tableScrollView.documentView = pathsTableView
|
|
|
|
cmdButton.target = self
|
|
optButton.target = self
|
|
ctrlButton.target = self
|
|
shiftButton.target = self
|
|
recordButton.delegate = self
|
|
launchAtLoginLabel.target = self
|
|
resetAllButton.target = self
|
|
|
|
recordButton.defaultKey = kVK_Space
|
|
|
|
recordButton.target = self
|
|
|
|
pathsTableView.dataSource = self
|
|
pathsTableView.delegate = self
|
|
|
|
pathsControl.target = self
|
|
pathsControl.action = #selector(affectPaths(_:))
|
|
|
|
pathsControl.target = self
|
|
pathsControl.action = #selector(affectPaths(_:))
|
|
|
|
launchAtLoginToggle.target = self
|
|
launchAtLoginToggle.action = #selector(affectLaunchAtLogin(_:))
|
|
|
|
keyboardEvents = LocalEventMonitor(mask: [.keyDown])
|
|
{ [weak self] event in
|
|
let key = event.keyCode
|
|
let modifiers = event.modifierFlags.rawValue
|
|
|
|
if modsContains(keys: OSCmd, in: modifiers) &&
|
|
key == kVK_ANSI_Q || modsContainsNone(in: modifiers)
|
|
{
|
|
NSApplication.shared.terminate(self)
|
|
}
|
|
|
|
return event
|
|
}
|
|
|
|
addSubviews()
|
|
setConstraints()
|
|
}
|
|
|
|
override func viewWillAppear() {
|
|
super.viewWillAppear()
|
|
|
|
// Fetch the saved key codes and modifiers.
|
|
if let code =
|
|
UserDefaults.standard.object(forKey: "keyCode") as? Int
|
|
{
|
|
keyCode = code
|
|
}
|
|
if let mods =
|
|
UserDefaults.standard.object(forKey: "keyModifiers") as? Int
|
|
{
|
|
modifiers = mods
|
|
}
|
|
|
|
loadPaths()
|
|
syncModifierButtons()
|
|
launchAtLoginStatus()
|
|
}
|
|
|
|
override func viewDidAppear() {
|
|
super.viewDidAppear()
|
|
|
|
keyboardEvents?.start()
|
|
|
|
NSApp.setActivationPolicy(.regular)
|
|
self.view.window?.center()
|
|
}
|
|
|
|
override func viewWillDisappear() {
|
|
super.viewWillDisappear()
|
|
|
|
keyboardEvents?.stop()
|
|
|
|
HotKeyManager.shared.registerHotKey(key: keyCode,
|
|
modifiers: modifiers)
|
|
|
|
UserDefaults.standard.set(keyCode, forKey: "keyCode")
|
|
UserDefaults.standard.set(modifiers, forKey: "keyModifiers")
|
|
|
|
// Merge PathManagers paths and user paths.
|
|
// WARNING: This seems a bit error prone.
|
|
for path in paths {
|
|
if !PathManager.shared.contains(path) {
|
|
PathManager.shared.addPath(path)
|
|
}
|
|
}
|
|
for path in PathManager.shared.paths {
|
|
if !paths.contains(path.key) {
|
|
PathManager.shared.removePath(path.key)
|
|
}
|
|
}
|
|
PathManager.shared.updateIndex()
|
|
PathManager.shared.savePaths()
|
|
}
|
|
|
|
override func viewDidDisappear() {
|
|
super.viewDidDisappear()
|
|
|
|
NSApp.setActivationPolicy(.accessory)
|
|
}
|
|
|
|
override func loadView() {
|
|
self.view = NSView()
|
|
}
|
|
|
|
@objc
|
|
private func showAbout() {
|
|
delegate.showAboutWindow()
|
|
}
|
|
|
|
@objc
|
|
private func handleModifiers() {
|
|
// Revert to default modifier if none of the modifier buttons are on.
|
|
if cmdButton.state != .on, optButton.state != .on,
|
|
ctrlButton.state != .on, shiftButton.state != .on
|
|
{
|
|
optButton.state = .on
|
|
}
|
|
|
|
detectModifers()
|
|
}
|
|
|
|
@objc
|
|
private func launchAtLogin(isOn status: Bool) {
|
|
delegate.toggleLaunchAtLogin(isOn: status)
|
|
launchAtLoginStatus()
|
|
}
|
|
|
|
private func launchAtLoginStatus() {
|
|
if delegate.willLaunchAtLogin() {
|
|
launchAtLoginToggle.setSelected(true, forSegment: 1)
|
|
} else {
|
|
launchAtLoginToggle.setSelected(true, forSegment: 0)
|
|
}
|
|
}
|
|
|
|
@objc
|
|
private func reset() {
|
|
keyCode = Int(kVK_Space)
|
|
modifiers = Int(optionKey)
|
|
HotKeyManager.shared.registerHotKey(key: keyCode,
|
|
modifiers: modifiers)
|
|
UserDefaults.standard.set(keyCode, forKey: "keyCode")
|
|
UserDefaults.standard.set(modifiers, forKey: "keyModifiers")
|
|
syncModifierButtons()
|
|
|
|
PathManager.shared.resetPaths()
|
|
loadPaths()
|
|
}
|
|
|
|
private func loadPaths() {
|
|
paths = []
|
|
for path in PathManager.shared.paths {
|
|
paths.append(path.key)
|
|
}
|
|
paths.sort()
|
|
pathsTableView.reloadData()
|
|
}
|
|
|
|
private func detectModifers() {
|
|
var mods = 0
|
|
|
|
if cmdButton.state == .on {
|
|
mods |= cmdKey
|
|
}
|
|
if optButton.state == .on {
|
|
mods |= optionKey
|
|
}
|
|
if ctrlButton.state == .on {
|
|
mods |= controlKey
|
|
}
|
|
if shiftButton.state == .on {
|
|
mods |= shiftKey
|
|
}
|
|
|
|
if mods == 0 {
|
|
mods |= optionKey
|
|
} else {
|
|
modifiers = mods
|
|
}
|
|
}
|
|
|
|
private func syncModifierButtons() {
|
|
ctrlButton.state = .off
|
|
cmdButton.state = .off
|
|
optButton.state = .off
|
|
shiftButton.state = .off
|
|
|
|
if modifiers & controlKey != 0 {
|
|
ctrlButton.state = .on
|
|
}
|
|
if modifiers & cmdKey != 0 {
|
|
cmdButton.state = .on
|
|
}
|
|
if modifiers & optionKey != 0 {
|
|
optButton.state = .on
|
|
}
|
|
if modifiers & shiftKey != 0 {
|
|
shiftButton.state = .on
|
|
}
|
|
|
|
if let character = keyName(virtualKeyCode: UInt16(keyCode)) {
|
|
recordButton.title = character
|
|
}
|
|
}
|
|
|
|
@objc
|
|
private func affectPaths(_ sender: NSSegmentedControl) {
|
|
let selectedSegment = sender.selectedSegment
|
|
switch selectedSegment {
|
|
case 0:
|
|
let row = paths.count
|
|
paths.append("")
|
|
pathsTableView.insertRows(at: IndexSet(integer: row),
|
|
withAnimation: [])
|
|
|
|
pathsTableView.scrollRowToVisible(row)
|
|
pathsTableView.selectRowIndexes(IndexSet(integer: row),
|
|
byExtendingSelection: false)
|
|
(pathsTableView
|
|
.view(atColumn: 0, row: row, makeIfNecessary: false
|
|
) as? PathsTableCellView)?.startEditing()
|
|
break
|
|
case 1:
|
|
if pathsTableView.selectedRow > -1 {
|
|
paths.remove(at: pathsTableView.selectedRow)
|
|
pathsTableView.reloadData()
|
|
}
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
@objc
|
|
private func affectLaunchAtLogin(_ sender: NSSegmentedControl) {
|
|
let selectedSegment = sender.selectedSegment
|
|
switch selectedSegment {
|
|
case 0:
|
|
launchAtLogin(isOn: false)
|
|
break
|
|
case 1:
|
|
launchAtLogin(isOn: true)
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
@objc
|
|
private func editItem(_ sender: NSTableView) {
|
|
pathsTableView.deselectAll(nil)
|
|
pathsTableView.selectRowIndexes(
|
|
IndexSet(integer: pathsTableView.clickedRow),
|
|
byExtendingSelection: false
|
|
)
|
|
|
|
if let cell = pathsTableView.view(atColumn: 0,
|
|
row: pathsTableView.clickedRow,
|
|
makeIfNecessary: false) as?
|
|
PathsTableCellView
|
|
{
|
|
cell.startEditing()
|
|
}
|
|
}
|
|
|
|
func titleFieldFinishedEditing(tag: Int, text: String) {
|
|
if text.isEmpty {
|
|
paths.remove(at: tag)
|
|
} else {
|
|
paths[tag] = text
|
|
}
|
|
pathsTableView.reloadData()
|
|
}
|
|
|
|
func titleFieldTextChanged(tag: Int, text: String) {
|
|
if let cell = pathsTableView.view(atColumn: 0, row: tag,
|
|
makeIfNecessary: false) as? PathsTableCellView
|
|
{
|
|
if isDirectory(text) {
|
|
cell.titleField.textColor = NSColor.systemGreen
|
|
} else {
|
|
cell.titleField.textColor = NSColor.systemRed
|
|
}
|
|
}
|
|
}
|
|
|
|
func keyWasSet(to keyCode: Int) {
|
|
self.keyCode = Int(keyCode)
|
|
}
|
|
|
|
func selectionButtonClicked(tag: Int) {
|
|
if dirPicker == nil {
|
|
dirPicker = NSOpenPanel()
|
|
dirPicker!.message = "Select a directory to search applications in..."
|
|
dirPicker!.canChooseDirectories = true
|
|
dirPicker!.canChooseFiles = false
|
|
dirPicker!.allowsMultipleSelection = false
|
|
}
|
|
|
|
// WARN:
|
|
// FIX: There is a bug where the program crashes when adding a new
|
|
// path. This happens because the settings popup is closed before
|
|
// displaying the selection modal, as a result the new path item
|
|
// is cleared (well, b/c it's empty) and the path gets set into
|
|
// non-existent memory which results in segmentation fault.
|
|
|
|
NSRunningApplication.current.activate(options: .activateAllWindows)
|
|
delegate.window.level = .normal
|
|
delegate.aboutWindow.performClose(nil)
|
|
|
|
if dirPicker!.runModal() == .OK {
|
|
if let url = dirPicker!.url {
|
|
paths[tag] = url.path
|
|
pathsTableView.reloadData()
|
|
}
|
|
}
|
|
|
|
delegate.window.level = .statusBar
|
|
delegate.window.makeKeyAndOrderFront(nil)
|
|
if let controller =
|
|
delegate.window.contentViewController as? SearchViewController
|
|
{
|
|
controller.openSettings()
|
|
}
|
|
}
|
|
|
|
func numberOfRows(in tableView: NSTableView) -> Int {
|
|
return paths.count
|
|
}
|
|
|
|
func tableView(_ tableView: NSTableView,
|
|
viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?
|
|
{
|
|
let rect = NSRect(x: 0, y: 0, width: tableColumn!.width,
|
|
height: 20)
|
|
let cell = PathsTableCellView(frame: rect)
|
|
cell.titleField.stringValue = paths[row]
|
|
cell.delegate = self
|
|
cell.id = row
|
|
return cell
|
|
}
|
|
}
|