Skip to content

Commit

Permalink
Select and deselect using checkbox
Browse files Browse the repository at this point in the history
  • Loading branch information
marcoconti83 committed Feb 20, 2018
1 parent b0cadf9 commit f0a43df
Show file tree
Hide file tree
Showing 9 changed files with 301 additions and 48 deletions.
12 changes: 12 additions & 0 deletions EasyTables.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
5415A6BD1F3F2396000FF9BB /* NSTableView+Easy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5415A6BC1F3F2396000FF9BB /* NSTableView+Easy.swift */; };
542578E81F6B1BD700FE6DA8 /* Cartography.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 542578E61F6B1BD300FE6DA8 /* Cartography.framework */; };
542578E91F6B1CA500FE6DA8 /* Cartography.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 542578E61F6B1BD300FE6DA8 /* Cartography.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5450685F203C02A600855939 /* SelectedObjects.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5450685E203C02A600855939 /* SelectedObjects.swift */; };
54CEC1D01F21D4DE00FA3BC6 /* EasyTables.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54CEC1C61F21D4DE00FA3BC6 /* EasyTables.framework */; };
54CEC1D51F21D4DE00FA3BC6 /* EasyTablesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CEC1D41F21D4DE00FA3BC6 /* EasyTablesTests.swift */; };
54CEC1D71F21D4DE00FA3BC6 /* EasyTables.h in Headers */ = {isa = PBXBuildFile; fileRef = 54CEC1C91F21D4DE00FA3BC6 /* EasyTables.h */; settings = {ATTRIBUTES = (Public, ); }; };
Expand All @@ -31,6 +32,7 @@
54DBCE9220363BF0001D86DF /* 3.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 54DBCE8E20363BF0001D86DF /* 3.jpg */; };
54DBCE9420363C25001D86DF /* RandomImageDirectory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54DBCE9320363C25001D86DF /* RandomImageDirectory.swift */; };
54DBCE962036468A001D86DF /* ObjectOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54DBCE952036468A001D86DF /* ObjectOperation.swift */; };
54DBCE98203646F7001D86DF /* SelectionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54DBCE97203646F7001D86DF /* SelectionModel.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -66,6 +68,9 @@
542578E21F6B18D800FE6DA8 /* Cartfile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Cartfile; sourceTree = "<group>"; };
542578E31F6B18D800FE6DA8 /* Cartfile.resolved */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Cartfile.resolved; sourceTree = "<group>"; };
542578E61F6B1BD300FE6DA8 /* Cartography.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cartography.framework; path = Carthage/Build/Mac/Cartography.framework; sourceTree = "<group>"; };
5450685E203C02A600855939 /* SelectedObjects.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedObjects.swift; sourceTree = "<group>"; };
5457D1722036BD01006028A0 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
5457D1732036BD01006028A0 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
54CEC1C61F21D4DE00FA3BC6 /* EasyTables.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = EasyTables.framework; sourceTree = BUILT_PRODUCTS_DIR; };
54CEC1C91F21D4DE00FA3BC6 /* EasyTables.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EasyTables.h; sourceTree = "<group>"; };
54CEC1CA1F21D4DE00FA3BC6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
Expand All @@ -86,6 +91,7 @@
54DBCE8E20363BF0001D86DF /* 3.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = 3.jpg; sourceTree = "<group>"; };
54DBCE9320363C25001D86DF /* RandomImageDirectory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomImageDirectory.swift; sourceTree = "<group>"; };
54DBCE952036468A001D86DF /* ObjectOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectOperation.swift; sourceTree = "<group>"; };
54DBCE97203646F7001D86DF /* SelectionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionModel.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -130,6 +136,8 @@
54CEC1BC1F21D4DE00FA3BC6 = {
isa = PBXGroup;
children = (
5457D1722036BD01006028A0 /* LICENSE */,
5457D1732036BD01006028A0 /* README.md */,
542578E21F6B18D800FE6DA8 /* Cartfile */,
542578E31F6B18D800FE6DA8 /* Cartfile.resolved */,
5415A6A91F3F0020000FF9BB /* Frameworks */,
Expand All @@ -156,6 +164,8 @@
54CEC1C91F21D4DE00FA3BC6 /* EasyTables.h */,
54CEC1CA1F21D4DE00FA3BC6 /* Info.plist */,
54CEC20D1F21D6D900FA3BC6 /* EasyTableSource.swift */,
54DBCE97203646F7001D86DF /* SelectionModel.swift */,
5450685E203C02A600855939 /* SelectedObjects.swift */,
54DBCE952036468A001D86DF /* ObjectOperation.swift */,
540403391F67B669000A7C79 /* ColumnDefinition.swift */,
54CEC20F1F21D85D00FA3BC6 /* GenericTableDataSource.swift */,
Expand Down Expand Up @@ -350,10 +360,12 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5450685F203C02A600855939 /* SelectedObjects.swift in Sources */,
54DBCE962036468A001D86DF /* ObjectOperation.swift in Sources */,
5404033A1F67B669000A7C79 /* ColumnDefinition.swift in Sources */,
5415A6BD1F3F2396000FF9BB /* NSTableView+Easy.swift in Sources */,
54CEC2101F21D85D00FA3BC6 /* GenericTableDataSource.swift in Sources */,
54DBCE98203646F7001D86DF /* SelectionModel.swift in Sources */,
5415A6B81F3F0FB5000FF9BB /* EasyTableSource.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
16 changes: 10 additions & 6 deletions EasyTables/ColumnDefinition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,23 +50,27 @@ public enum ColumnWidth {
public struct ColumnDefinition<Object> {

/// Name of the column
public var name: String
public let name: String
/// Derive the value to display from the object
public var value: (Object)->(Any)
public let value: (Object)->(Any)
/// Comparison operator
public var comparison: (Object, Object)->ComparisonResult
public let comparison: (Object, Object)->ComparisonResult
/// Witdh of the column
public var width: ColumnWidth
public let width: ColumnWidth
/// Alignment
public var alignment: NSTextAlignment

public let alignment: NSTextAlignment
/// Internal ID
private let generatedID: UUID = UUID()
/// Column identifier
let identifier: String

public init(name: String,
width: ColumnWidth = .M,
alignment: NSTextAlignment = .left,
comparison: ((Object, Object)->ComparisonResult)? = nil,
value: @escaping (Object)->(Any)
) {
self.identifier = (name + "_" + self.generatedID.uuidString).lowercased()
self.name = name
self.value = value
self.width = width
Expand Down
66 changes: 38 additions & 28 deletions EasyTables/EasyTableSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ let TextCellViewIdentifier = "EasyDialogs_TextCellViewIdentifier"
/// At initialization, it is associated with a specific instance of NSTableView
/// and will create the necessary columns on that NSTableView
public class EasyTableSource<Object: Equatable> {

/// Internal data source associated with the table
public let dataSource: GenericTableDataSource<Object>
private(set) public var dataSource: GenericTableDataSource<Object>! = nil

public var table: NSTableView {
return self.dataSource.table
Expand All @@ -48,34 +48,31 @@ public class EasyTableSource<Object: Equatable> {
/// when right-clicking on a table row
/// - parameter table: the table to apply this configuration to. If not specified,
/// will create a new one
/// - parameter allowMultipleSelection: whether multiple rows can be selected in the table
/// - parameter selectionModel: whether multiple rows can be selected in the table
/// - parameter selectionCallback: callback invoked when the selection changes

public init<Objects: Collection>(initialObjects: Objects,
columns: [ColumnDefinition<Object>],
contextMenuOperations: [ObjectOperation<Object>],
table: NSTableView? = nil,
allowMultipleSelection: Bool,
selectionCallback: @escaping ([Object])->(Void))
selectionModel: SelectionModel = .singleNative,
selectionCallback: (([Object])->(Void))?)
where Objects.Iterator.Element == Object
{
let columns = (selectionModel.requiresCheckboxColumn ? [self.checkboxColumn] : []) + columns
let table = table ?? NSTableView()
self.dataSource = GenericTableDataSource(
initialObjects: Array(initialObjects),
columns: columns,
contextMenuOperations: contextMenuOperations,
table: table,
selectionCallback: selectionCallback
selectionModel: selectionModel,
selectionCallback: selectionCallback ?? { _ in }
)

table.dataSource = self.dataSource
table.delegate = self.dataSource

var columnsLookup: [String: ColumnDefinition<Object>] = [:]
columns.forEach {
columnsLookup[$0.name.lowercased()] = $0
}

self.setupTable(columns: columns, multiSelection: allowMultipleSelection)
self.setupTable(columns: columns, selectionModel: selectionModel)
self.setupMenu(operations: contextMenuOperations)
}

Expand All @@ -95,32 +92,29 @@ extension EasyTableSource {
/// Sets up table columns and selection methods
fileprivate func setupTable(
columns: [ColumnDefinition<Object>],
multiSelection: Bool)
selectionModel: SelectionModel)
{
let preColumns = self.table.tableColumns
preColumns.forEach {
self.table.removeTableColumn($0)
}

columns.forEach { cdef in
let column = NSTableColumn()
column.title = cdef.name
column.identifier = NSUserInterfaceItemIdentifier(rawValue: cdef.name.lowercased())
column.identifier = NSUserInterfaceItemIdentifier(rawValue: cdef.identifier)
column.isEditable = false
column.minWidth = cdef.width.width
column.maxWidth = cdef.width.width * 2
column.sortDescriptorPrototype = NSSortDescriptor(key: column.title, ascending: false) {
column.sortDescriptorPrototype = NSSortDescriptor(key: cdef.identifier, ascending: false) {
let value1 = cdef.value($0 as! Object)
let value2 = cdef.value($1 as! Object)
return "\(value1)".compare("\(value2)")
}
table.addTableColumn(column)
}

self.table.allowsEmptySelection = true
self.table.allowsColumnSelection = false
self.table.allowsTypeSelect = true
self.table.allowsMultipleSelection = multiSelection
selectionModel.configureTableSelectionProperties(self.table)
}

/// Sets up the contextual menu for the table
Expand All @@ -145,18 +139,34 @@ extension EasyTableSource {
self.table.menu = menu
}

private var checkboxColumn: ColumnDefinition<Object> {
return ColumnDefinition(name: "",
width: .custom(10),
alignment: .center)
{ obj in
let checkbox = ClosureButton()
checkbox.closure = { [weak self, weak checkbox] _ in
guard let `self` = self, let checkbox = checkbox else { return }
let isSelected = checkbox.state == NSControl.StateValue.on
self.dataSource.checkboxSelected[obj] = isSelected
}
checkbox.setButtonType(.switch)
checkbox.title = ""
checkbox.state = self.dataSource.isSelected(obj) ? NSControl.StateValue.on : NSControl.StateValue.off
return checkbox
}
}

/// Objects that should be affected by a contextual operation
fileprivate var targetObjectsForContextualOperation: [Object] {
let selectedIndex = self.dataSource.table.selectedRowIndexes
let clickedIndex = self.dataSource.table.clickedRow
let selectedObjects = self.dataSource.selectedItems // TODO change to objs
let clickedObject = self.dataSource.value(row: self.dataSource.table.clickedRow)

let indexesToUse: [Int]
if clickedIndex != -1 && !selectedIndex.contains(clickedIndex) {
indexesToUse = [clickedIndex]
if let clicked = clickedObject, !selectedObjects.contains(clicked) {
return [clicked]
} else {
indexesToUse = selectedIndex.map { $0 }
return selectedObjects
}
return indexesToUse.flatMap(self.dataSource.value(row:))
}
}

Expand Down
63 changes: 51 additions & 12 deletions EasyTables/GenericTableDataSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,36 @@ public class GenericTableDataSource<Object: Equatable>: NSObject, NSTableViewDel
}
}

/// List of objects currently selected with checkbox
var checkboxSelected = SelectedObjects<Object>()

/// Checkbox selection change listener
private var checkboxSelectionListenerToken: Any? = nil

/// Selection model for the table
let selectionModel: SelectionModel

init(initialObjects: [Object],
columns: [ColumnDefinition<Object>],
contextMenuOperations: [ObjectOperation<Object>] = [],
table: NSTableView,
selectionModel: SelectionModel,
selectionCallback: @escaping ([Object])->(Void) = { _ in }
) {
self.filter = nil
self.table = table
var columnsLookup: [String: ColumnDefinition<Object>] = [:]
columns.forEach {
columnsLookup[$0.name.lowercased()] = $0
}
self.columns = columnsLookup
self.selectionModel = selectionModel
self.columns = Dictionary(
columns.map { ($0.identifier, $0) },
uniquingKeysWith: { a, b in a })
self.selectionCallback = selectionCallback
super.init()
self.update(newObjects: initialObjects)
self.checkboxSelectionListenerToken = self.checkboxSelected.addObserver {
[weak self] _ in
guard let `self` = self else { return }
self.selectionCallback(self.selectedItems)
}
self.update(newObjects: initialObjects, invokeSelectionCallback: false)
}

public func numberOfRows(in tableView: NSTableView) -> Int {
Expand Down Expand Up @@ -127,11 +141,17 @@ public class GenericTableDataSource<Object: Equatable>: NSObject, NSTableViewDel
self.selectionCallback(self.selectedItems)
}

public func selectionShouldChange(in tableView: NSTableView) -> Bool {
return !self.selectionModel.blocksNativeSelection
}

/// Update the objects, re-apply filter and sorting
func update(newObjects: [Object]) {
func update(newObjects: [Object], invokeSelectionCallback: Bool = true) {
self.originalObjects = newObjects
self.recalculateSource()
self.selectionCallback(self.selectedItems)
if invokeSelectionCallback {
self.selectionCallback(self.selectedItems)
}
}

/// Refilter original objects then sort them
Expand Down Expand Up @@ -198,14 +218,33 @@ public class GenericTableDataSource<Object: Equatable>: NSObject, NSTableViewDel
public func select<SEQUENCE: Sequence>(items: SEQUENCE, extendSelection: Bool = false)
where SEQUENCE.Iterator.Element == Object
{
let indexes = items.flatMap { item in self.sortedObjects.index(where: { $0 == item }) }
self.table.selectRowIndexes(IndexSet(indexes), byExtendingSelection: extendSelection)
if self.selectionModel.requiresManualSelectionTracking {
if !extendSelection {
self.checkboxSelected.setSelection(items)
} else {
self.checkboxSelected.select(items)
}
self.table.reloadData()
} else {
let indexes = items.flatMap { item in self.sortedObjects.index(where: { $0 == item }) }
self.table.selectRowIndexes(IndexSet(indexes), byExtendingSelection: extendSelection)
}
}

/// The items currently selected in the table
public var selectedItems: [Object] {
return self.table.selectedRowIndexes.map {
self.sortedObjects[$0]
if self.selectionModel.requiresManualSelectionTracking {
return self.checkboxSelected.selectedObjects
} else {
return self.table.selectedRowIndexes.map {
self.sortedObjects[$0]
}
}
}

/// Whether an object is selected
/// - note: this will cause two linear scans of the elements
func isSelected(_ object: Object) -> Bool {
return self.selectedItems.contains(object)
}
}
Loading

0 comments on commit f0a43df

Please sign in to comment.