Skip to content

Commit

Permalink
Merge pull request #567 from zenangst/improve/click-interactions-on-m…
Browse files Browse the repository at this point in the history
…acos

Improve handling mouse clicks on macOS
  • Loading branch information
zenangst committed Mar 28, 2017
2 parents a120411 + ae2df3d commit deeac80
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 43 deletions.
6 changes: 3 additions & 3 deletions Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
github "krzyzanowskim/CryptoSwift" "0.6.7"
github "ReactiveX/RxSwift" "3.2.0"
github "ReactiveX/RxSwift" "3.3.1"
github "hyperoslo/Cache" "2.2.2"
github "krzyzanowskim/CryptoSwift" "0.6.8"
github "zenangst/Tailor" "2.0.2"
github "hyperoslo/Cache" "2.2.1"
23 changes: 20 additions & 3 deletions Sources/Shared/Structs/Interaction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ enum ScrollDirection: String {
case horizontal, vertical
}

/// Configures what kind of click behavior the component should use.
///
/// - single: Single mouse click.
/// - double: Double mouse click (only supported on components that use table views).
public enum MouseClick: String {
case single, double
}

/// A user interaction struct used for mapping behavior to a component.
/// Note: `paginate` is currently only available on iOS.
public struct Interaction: Mappable {
Expand All @@ -17,20 +25,23 @@ public struct Interaction: Mappable {
///
/// - paginate: Used for mapping pagination behavior.
enum Key: String {
case paginate
case paginate, mouseClick
}

/// Delcares what kind of interaction should be used for pagination. See `Paginate` struct for more information.
var paginate: Paginate = .disabled
/// Indicates which scrolling direction will be used, default to false.
var scrollDirection: ScrollDirection = .vertical
/// Indicates what kind click interaction the element should use.
var mouseClick: MouseClick = .single

/// The root key used when parsing JSON into a Interaction struct.
static let rootKey: String = String(describing: Interaction.self).lowercased()

/// A dictionary representation of the struct.
public var dictionary: [String : Any] {
return [
Key.mouseClick.rawValue: mouseClick.rawValue,
Key.paginate.rawValue: paginate.rawValue
]
}
Expand All @@ -43,8 +54,9 @@ public struct Interaction: Mappable {
/// Default initializer for creating a Interaction struct.
///
/// - Parameter paginate: Declares which pagination behavior that should be used, `.disabled` is default.
public init(paginate: Paginate = .disabled) {
public init(paginate: Paginate = .disabled, mouseClick: MouseClick = .single) {
self.paginate = paginate
self.mouseClick = mouseClick
}

/// Initialize with a JSON payload.
Expand All @@ -61,6 +73,10 @@ public struct Interaction: Mappable {
if let paginate: String = map.property(Key.paginate.rawValue) {
self.paginate <- Paginate(rawValue: paginate)
}

if let mouseClick: String = map.property(Key.mouseClick.rawValue) {
self.mouseClick <- MouseClick(rawValue: mouseClick)
}
}

/// Compare Interaction structs.
Expand All @@ -70,7 +86,8 @@ public struct Interaction: Mappable {
/// - rhs: Right hand side Interaction
/// - Returns: A boolean value that is true if all properties are equal on the struct.
public static func == (lhs: Interaction, rhs: Interaction) -> Bool {
return lhs.paginate == rhs.paginate
return lhs.paginate == rhs.paginate &&
lhs.mouseClick == rhs.mouseClick
}

/// Compare Interaction structs.
Expand Down
22 changes: 16 additions & 6 deletions Sources/macOS/Classes/Component.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,18 +156,18 @@ import Tailor
/// A convenience init for creating a component with a `ComponentModel`.
///
/// - Parameter model: A component model that is used for constructing and configurating the component.
public required convenience init(model: ComponentModel) {
let userInterface: UserInterface
public required convenience init(model: ComponentModel, userInterface: UserInterface? = nil) {
var userInterface: UserInterface! = userInterface

if model.kind == .list {
if userInterface == nil, model.kind == .list {
userInterface = TableView()
} else {
let collectionView = CollectionView(frame: CGRect.zero)
collectionView.collectionViewLayout = GridableLayout()
userInterface = collectionView
}

self.init(model: model, userInterface: userInterface)
self.init(model: model, userInterface: userInterface!)

if model.kind == .carousel {
self.model.interaction.scrollDirection = .horizontal
Expand Down Expand Up @@ -301,22 +301,32 @@ import Tailor
/// This method is invoked when a double click is performed on a view.
///
/// - Parameter sender: The view that was tapped.
open func doubleAction(_ sender: Any?) {
open func doubleMouseClick(_ sender: Any?) {
guard let tableView = tableView,
let item = item(at: tableView.clickedRow) else {
return
}

guard model.interaction.mouseClick == .double else {
return
}

delegate?.component(self, itemSelected: item)
}

/// This method is invoked when a single click is performed on a view.
///
/// - Parameter sender: The view that was tapped.
open func action(_ sender: Any?) {
open func singleMouseClick(_ sender: Any?) {
guard let tableView = tableView,
let item = item(at: tableView.clickedRow) else {
return
}

guard model.interaction.mouseClick == .single else {
return
}

delegate?.component(self, itemSelected: item)
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/macOS/Extensions/Component+macOS+List.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ extension Component {
tableView.allowsTypeSelect = true
tableView.focusRingType = .none
tableView.target = self
tableView.action = #selector(self.action(_:))
tableView.doubleAction = #selector(self.doubleAction(_:))
tableView.action = #selector(self.singleMouseClick(_:))
tableView.doubleAction = #selector(self.doubleMouseClick(_:))
tableView.sizeToFit()

guard tableView.tableColumns.isEmpty else {
Expand Down
15 changes: 0 additions & 15 deletions Sources/macOS/Extensions/Delegate+macOS+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,21 +69,6 @@ extension Delegate: NSCollectionViewDelegateFlowLayout {

extension Delegate: NSTableViewDelegate {

public func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool {
guard let component = component,
let item = component.item(at: row),
row > -1 && row < component.model.items.count
else {
return false
}

if component.model.meta("double-action", type: Bool.self) != true {
component.delegate?.component(component, itemSelected: item)
}

return true
}

public func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
guard let component = component else {
return 1.0
Expand Down
28 changes: 17 additions & 11 deletions Spots.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@
BD4295551D81D39700E07E1C /* TestComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4295541D81D39700E07E1C /* TestComponent.swift */; };
BD42CA851E4C9B2600A86E3B /* TestSpot.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD42CA841E4C9B2600A86E3B /* TestSpot.swift */; };
BD42CA861E4C9B2600A86E3B /* TestSpot.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD42CA841E4C9B2600A86E3B /* TestSpot.swift */; };
BD43459B1E35EB7D0032EB3E /* TestInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4345981E35EB470032EB3E /* TestInteraction.swift */; };
BD43459C1E35EB7E0032EB3E /* TestInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4345981E35EB470032EB3E /* TestInteraction.swift */; };
BD45D9871E30906300C2D6B2 /* TestLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD45D9861E30906300C2D6B2 /* TestLayout.swift */; };
BD45D9881E30906300C2D6B2 /* TestLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD45D9861E30906300C2D6B2 /* TestLayout.swift */; };
BD45D9891E30906300C2D6B2 /* TestLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD45D9861E30906300C2D6B2 /* TestLayout.swift */; };
Expand Down Expand Up @@ -300,6 +298,10 @@
BDDF2CCC1DC7C23500B766BA /* TestAnimations.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDDF2CCB1DC7C23500B766BA /* TestAnimations.swift */; };
BDDF2CCD1DC7C23500B766BA /* TestAnimations.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDDF2CCB1DC7C23500B766BA /* TestAnimations.swift */; };
BDDF2CD01DC7C50700B766BA /* TestAnimations.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDDF2CCF1DC7C50700B766BA /* TestAnimations.swift */; };
BDEA327C1E86FC850044B056 /* TestClickInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDEA327B1E86FC850044B056 /* TestClickInteraction.swift */; };
BDEA327F1E87A6FF0044B056 /* TestInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDEA327E1E87A6FF0044B056 /* TestInteraction.swift */; };
BDEA32801E87A6FF0044B056 /* TestInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDEA327E1E87A6FF0044B056 /* TestInteraction.swift */; };
BDEA32811E87A6FF0044B056 /* TestInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDEA327E1E87A6FF0044B056 /* TestInteraction.swift */; };
BDEED2E81D8446400030B475 /* TestSpotsScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDEED2E71D8446400030B475 /* TestSpotsScrollView.swift */; };
BDEFF54F1DD1C85300FC0537 /* TestDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDEFF54E1DD1C85300FC0537 /* TestDataSource.swift */; };
BDEFF5501DD1C85300FC0537 /* TestDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDEFF54E1DD1C85300FC0537 /* TestDataSource.swift */; };
Expand Down Expand Up @@ -387,7 +389,6 @@
BD2403101E4B9A02005BAA19 /* Component.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Component.swift; sourceTree = "<group>"; };
BD4295541D81D39700E07E1C /* TestComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestComponent.swift; sourceTree = "<group>"; };
BD42CA841E4C9B2600A86E3B /* TestSpot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestSpot.swift; sourceTree = "<group>"; };
BD4345981E35EB470032EB3E /* TestInteraction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestInteraction.swift; sourceTree = "<group>"; };
BD45D9861E30906300C2D6B2 /* TestLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestLayout.swift; sourceTree = "<group>"; };
BD45D98A1E30909700C2D6B2 /* TestLayoutExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestLayoutExtensions.swift; sourceTree = "<group>"; };
BD45D98E1E30935500C2D6B2 /* TestLayoutExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestLayoutExtensions.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -511,6 +512,8 @@
BDDCF6EE1E4DF93D004B38C4 /* DictionaryConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryConvertible.swift; sourceTree = "<group>"; };
BDDF2CCB1DC7C23500B766BA /* TestAnimations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestAnimations.swift; sourceTree = "<group>"; };
BDDF2CCF1DC7C50700B766BA /* TestAnimations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestAnimations.swift; sourceTree = "<group>"; };
BDEA327B1E86FC850044B056 /* TestClickInteraction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestClickInteraction.swift; sourceTree = "<group>"; };
BDEA327E1E87A6FF0044B056 /* TestInteraction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestInteraction.swift; sourceTree = "<group>"; };
BDEED2E71D8446400030B475 /* TestSpotsScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestSpotsScrollView.swift; sourceTree = "<group>"; };
BDEFF54E1DD1C85300FC0537 /* TestDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestDataSource.swift; sourceTree = "<group>"; };
BDEFF5511DD1DA8000FC0537 /* TestDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestDelegate.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -891,6 +894,7 @@
BD45D98E1E30935500C2D6B2 /* TestLayoutExtensions.swift */,
BD165A361E6EAAA60023AF82 /* TestSpot.swift */,
BD165A381E6EAD750023AF82 /* HelperViews.swift */,
BDEA327B1E86FC850044B056 /* TestClickInteraction.swift */,
);
path = macOS;
sourceTree = "<group>";
Expand Down Expand Up @@ -986,7 +990,6 @@
BDEFF54E1DD1C85300FC0537 /* TestDataSource.swift */,
BDEFF5511DD1DA8000FC0537 /* TestDelegate.swift */,
BDD9ABCA1DB533CE005D8C04 /* TestGridComposite.swift */,
BD4345981E35EB470032EB3E /* TestInteraction.swift */,
BD45D98A1E30909700C2D6B2 /* TestLayoutExtensions.swift */,
BD01BD141DAEA7EE009C10FF /* TestListComposite.swift */,
BDEED2E71D8446400030B475 /* TestSpotsScrollView.swift */,
Expand All @@ -1005,15 +1008,16 @@
BD677E8E1DC65D63006D1654 /* Helpers.swift */,
D584782B1C43FF34006EBA49 /* TestComponentModel.swift */,
BD6FBEEF1E12B5F000AA58BD /* TestComposition.swift */,
BDCFCD401DCA7EFF0047E84C /* TestSpotsController.swift */,
BD45D9861E30906300C2D6B2 /* TestLayout.swift */,
BD01BD0D1DAEA464009C10FF /* TestParser.swift */,
BD677E841DC616B2006D1654 /* TestCoreComponent.swift */,
BD677E881DC61EFC006D1654 /* TestStateCache.swift */,
BD10D5211D79533C00DF8E9B /* TestViewModelExtensions.swift */,
BD45D9941E30A8A000C2D6B2 /* TestInset.swift */,
BDEA327E1E87A6FF0044B056 /* TestInteraction.swift */,
BDB8D5921E4DFADC00220BC3 /* TestItem.swift */,
BD45D9861E30906300C2D6B2 /* TestLayout.swift */,
BD01BD0D1DAEA464009C10FF /* TestParser.swift */,
BDCFCD401DCA7EFF0047E84C /* TestSpotsController.swift */,
BD677E881DC61EFC006D1654 /* TestStateCache.swift */,
BDCA3CF21E8295EB00A98A76 /* TestUserInterface.swift */,
BD10D5211D79533C00DF8E9B /* TestViewModelExtensions.swift */,
);
path = Shared;
sourceTree = "<group>";
Expand Down Expand Up @@ -1546,14 +1550,14 @@
buildActionMask = 2147483647;
files = (
BD45D98C1E30909700C2D6B2 /* TestLayoutExtensions.swift in Sources */,
BD43459C1E35EB7E0032EB3E /* TestInteraction.swift in Sources */,
BDEFF5501DD1C85300FC0537 /* TestDataSource.swift in Sources */,
BD45D9891E30906300C2D6B2 /* TestLayout.swift in Sources */,
BDCFCD431DCA7EFF0047E84C /* TestSpotsController.swift in Sources */,
BD677E8B1DC61EFC006D1654 /* TestStateCache.swift in Sources */,
BD7397381D718CDB000AF2DE /* TestComponentModel.swift in Sources */,
BD42CA861E4C9B2600A86E3B /* TestSpot.swift in Sources */,
BDB8D5951E4DFADC00220BC3 /* TestItem.swift in Sources */,
BDEA32811E87A6FF0044B056 /* TestInteraction.swift in Sources */,
D5D282741E547742004BF251 /* TestListWrapper.swift in Sources */,
BD21C2551E4358CE00FE2B26 /* TestGridableLayout.swift in Sources */,
BDEFF5531DD1DA8000FC0537 /* TestDelegate.swift in Sources */,
Expand Down Expand Up @@ -1683,7 +1687,6 @@
D5D282761E547D1D004BF251 /* TestGridWrapper.swift in Sources */,
BD4295551D81D39700E07E1C /* TestComponent.swift in Sources */,
D58478571C43FFFD006EBA49 /* TestComponentModel.swift in Sources */,
BD43459B1E35EB7D0032EB3E /* TestInteraction.swift in Sources */,
BD45D98B1E30909700C2D6B2 /* TestLayoutExtensions.swift in Sources */,
BDDF2CCC1DC7C23500B766BA /* TestAnimations.swift in Sources */,
BDEED2E81D8446400030B475 /* TestSpotsScrollView.swift in Sources */,
Expand All @@ -1705,6 +1708,7 @@
BD21C2541E4358CD00FE2B26 /* TestGridableLayout.swift in Sources */,
BDCA3CF31E8295EB00A98A76 /* TestUserInterface.swift in Sources */,
BD45D9951E30A8A000C2D6B2 /* TestInset.swift in Sources */,
BDEA327F1E87A6FF0044B056 /* TestInteraction.swift in Sources */,
BDD9ABCB1DB533CE005D8C04 /* TestGridComposite.swift in Sources */,
BDEFF5521DD1DA8000FC0537 /* TestDelegate.swift in Sources */,
);
Expand Down Expand Up @@ -1803,6 +1807,7 @@
BD677E901DC65D63006D1654 /* Helpers.swift in Sources */,
BDCA3CF41E8295EB00A98A76 /* TestUserInterface.swift in Sources */,
BD10D5261D7955AC00DF8E9B /* TestViewModelExtensions.swift in Sources */,
BDEA32801E87A6FF0044B056 /* TestInteraction.swift in Sources */,
BD165A391E6EAD750023AF82 /* HelperViews.swift in Sources */,
BD9AB9F61E44AD9700085677 /* TestCoreComponent.swift in Sources */,
BDCFCD421DCA7EFF0047E84C /* TestSpotsController.swift in Sources */,
Expand All @@ -1815,6 +1820,7 @@
BD6FBEF11E12B5F000AA58BD /* TestComposition.swift in Sources */,
BDFC47521E747B2B008700BF /* TestListWrapper.swift in Sources */,
BDB8D5941E4DFADC00220BC3 /* TestItem.swift in Sources */,
BDEA327C1E86FC850044B056 /* TestClickInteraction.swift in Sources */,
BDDF2CD01DC7C50700B766BA /* TestAnimations.swift in Sources */,
BD677E8A1DC61EFC006D1654 /* TestStateCache.swift in Sources */,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,26 +36,32 @@ class InteractionTests: XCTestCase {
XCTAssertEqual(interaction.paginate, .page)

json = [
"paginate": "item"
"paginate": "item",
"mouseClick" : "single"
]

interaction = Interaction(json)
XCTAssertEqual(interaction.paginate, .item)
XCTAssertEqual(interaction.mouseClick, .single)

json = [
"paginate": "disabled"
"paginate": "disabled",
"mouseClick": "double"
]

interaction = Interaction(json)
XCTAssertEqual(interaction.paginate, .disabled)
XCTAssertEqual(interaction.mouseClick, .double)

interaction = Interaction()
XCTAssertEqual(interaction.paginate, .disabled)
XCTAssertEqual(interaction.mouseClick, .single)
}

func testDictionary() {
let json: [String : Any] = [
"paginate": "page"
"paginate": "page",
"mouseClick" : "single"
]

let interaction = Interaction(json)
Expand Down
68 changes: 68 additions & 0 deletions SpotsTests/macOS/TestClickInteraction.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
@testable import Spots
import XCTest

class MockTableView: NSTableView {

var rowClicked: Int = -1

override var clickedRow: Int {

if rowView(atRow: rowClicked, makeIfNecessary: false) != nil {
return rowClicked
} else {
return -1
}
}
}

class MockComponentDelegate: NSObject, ComponentDelegate {

var invocation: Int = 0

func component(_ component: Component, itemSelected item: Item) {
invocation += 1
}
}

class TestMouseClick: XCTestCase {

func testMouseClick() {
let mockTableView = MockTableView()
let mockDelegate = MockComponentDelegate()
let interaction = Interaction(mouseClick: .single)
let model = ComponentModel(kind: .list, interaction: interaction, items: [
Item(title: "foo")
]
)
let component = Component(model: model, userInterface: mockTableView)
component.setup(with: CGSize(width: 100, height: 100))

component.delegate = mockDelegate

guard let tableView = component.tableView as? MockTableView else {
XCTFail("Unable to resolve table view.")
return
}

tableView.rowClicked = 0

/// Expect that the component delegate will be called once.
component.singleMouseClick(nil)
XCTAssertEqual(mockDelegate.invocation, 1)

/// Expect that the invocation count is still one after double clicking.
component.doubleMouseClick(nil)
XCTAssertEqual(mockDelegate.invocation, 1)

/// Expect that the invocation is unchanged when the configuration is set
/// to only accept double clicks.
component.model.interaction.mouseClick = .double
component.singleMouseClick(nil)
XCTAssertEqual(mockDelegate.invocation, 1)

/// Expect that invocation is two because it is inline with the configuration.
component.doubleMouseClick(nil)
XCTAssertEqual(mockDelegate.invocation, 2)
}

}

0 comments on commit deeac80

Please sign in to comment.