Skip to content

Commit ed40408

Browse files
committed
Implement unassigning key bindings
This allows the user to disable key bindings if they so prefer.
1 parent 3e9afdf commit ed40408

5 files changed

Lines changed: 167 additions & 35 deletions

File tree

KeyCommandKit/KeyBinding.swift

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
import Foundation
99

10+
let UnassignedKeyBindingInput = "Unassigned"
11+
1012
public class KeyBinding
1113
{
1214
/// An internal identifier. Must be unique, and is used to query key bindings. It is never shown to the user.
@@ -30,8 +32,21 @@ public class KeyBinding
3032
return false
3133
}
3234

33-
public func make(withAction action: Selector) -> UIKeyCommand
35+
/// Whether this is a customization where the user unassigned the key binding, effectively deactivating it.
36+
var isUnassigned: Bool
37+
{
38+
return input == UnassignedKeyBindingInput
39+
}
40+
41+
/// Makes a UIKeyCommand instance by attaching an action to the receiver binding. If the receiver binding was
42+
/// customized to "Unassigned" by the user, this method returns `nil`.
43+
public func make(withAction action: Selector) -> UIKeyCommand?
3444
{
45+
if isUnassigned
46+
{
47+
return nil
48+
}
49+
3550
if isDiscoverable, #available(iOS 9.0, *)
3651
{
3752
return UIKeyCommand(input: input, modifierFlags: modifiers, action: action, discoverabilityTitle: name)
@@ -138,21 +153,25 @@ internal extension KeyBinding
138153

139154
func customized(input: String, modifiers: UIKeyModifierFlags) -> KeyBinding
140155
{
141-
return CustomizedKeyBinding(key: key, name: name, input: input, modifiers: modifiers, isDiscoverable: isDiscoverable, originalInput: self.input, originalModifiers: self.modifiers)
156+
return CustomizedKeyBinding(key: key, name: name, input: input, modifiers: modifiers,
157+
isDiscoverable: isDiscoverable, originalInput: self.input,
158+
originalModifiers: self.modifiers)
142159
}
143160
}
144161

145162
public extension Dictionary where Key == String, Value == KeyBinding
146163
{
164+
/// Makes UIKeyCommand objects by attaching key bindings to actions by matching their respective `key`s.
165+
/// If a key binding was customized to "Unassigned" by the user, then this routine skips it.
147166
public func make(withActionsForKeys tuples: [(key: String, action: Selector)]) -> [UIKeyCommand]
148167
{
149168
var keyCommands = [UIKeyCommand]()
150169

151170
for (key, action) in tuples
152171
{
153-
if let binding = self[key]
172+
if let binding = self[key], let keyCommand = binding.make(withAction: action)
154173
{
155-
keyCommands.append(binding.make(withAction: action))
174+
keyCommands.append(keyCommand)
156175
}
157176
}
158177

KeyCommandKit/KeyBindingEditorView.xib

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
<dependencies>
77
<deployment identifier="iOS"/>
88
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
9+
<capability name="Alignment constraints with different attributes" minToolsVersion="5.1"/>
910
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
1011
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
1112
</dependencies>
1213
<objects>
1314
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="KeyBindingEditorViewController" customModule="KeyCommandKit" customModuleProvider="target">
1415
<connections>
15-
<outlet property="_instructionsLabel" destination="RlH-JE-Uvl" id="DHM-Gk-ZWE"/>
1616
<outlet property="view" destination="iN0-l3-epB" id="iMO-eS-1Xk"/>
1717
</connections>
1818
</placeholder>
@@ -27,10 +27,10 @@
2727
<nil key="textColor"/>
2828
<nil key="highlightedColor"/>
2929
</label>
30-
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="zhT-CX-EI5">
31-
<rect key="frame" x="107" y="147" width="60" height="33"/>
30+
<button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="zhT-CX-EI5">
31+
<rect key="frame" x="33.5" y="147" width="100" height="33"/>
3232
<constraints>
33-
<constraint firstAttribute="width" constant="60" id="2qe-mw-2gm"/>
33+
<constraint firstAttribute="width" constant="100" id="2qe-mw-2gm"/>
3434
</constraints>
3535
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
3636
<state key="normal" title="Save"/>
@@ -72,18 +72,43 @@
7272
<outlet property="modifiersStackView" destination="KL9-Xr-MOw" id="0hb-LS-JeH"/>
7373
</connections>
7474
</view>
75+
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="&lt;none&gt;" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ggp-Ki-OxV">
76+
<rect key="frame" x="92" y="84.5" width="91" height="31.5"/>
77+
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="26"/>
78+
<nil key="textColor"/>
79+
<nil key="highlightedColor"/>
80+
</label>
81+
<button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Gb8-qr-Wmv">
82+
<rect key="frame" x="141.5" y="147" width="100" height="33"/>
83+
<constraints>
84+
<constraint firstAttribute="width" constant="100" id="3jO-TZ-Di5"/>
85+
</constraints>
86+
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
87+
<state key="normal" title="Unassign"/>
88+
<connections>
89+
<action selector="unassign:" destination="iN0-l3-epB" eventType="touchUpInside" id="Lpc-ZB-XPs"/>
90+
</connections>
91+
</button>
7592
</subviews>
7693
<constraints>
94+
<constraint firstItem="Gb8-qr-Wmv" firstAttribute="leading" secondItem="zhT-CX-EI5" secondAttribute="trailing" constant="8" id="1v8-0B-pHV"/>
7795
<constraint firstAttribute="bottom" secondItem="zhT-CX-EI5" secondAttribute="bottom" constant="20" id="F6v-6s-4tC"/>
7896
<constraint firstItem="PoS-C4-kmO" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="IeF-Ek-cK8"/>
97+
<constraint firstItem="Ggp-Ki-OxV" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="QTi-7I-Q9D"/>
7998
<constraint firstItem="RlH-JE-Uvl" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="topMargin" constant="20" id="Rgu-4p-LQU"/>
8099
<constraint firstItem="RlH-JE-Uvl" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="VSB-KA-Cay"/>
81100
<constraint firstItem="PoS-C4-kmO" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="WR4-a5-lhg"/>
82-
<constraint firstItem="zhT-CX-EI5" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="wLr-0x-V9T"/>
101+
<constraint firstItem="Ggp-Ki-OxV" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="bZC-fd-5Ng"/>
102+
<constraint firstItem="Gb8-qr-Wmv" firstAttribute="baseline" secondItem="zhT-CX-EI5" secondAttribute="baseline" id="vji-WC-vex"/>
103+
<constraint firstItem="zhT-CX-EI5" firstAttribute="trailing" secondItem="iN0-l3-epB" secondAttribute="centerX" constant="-4" id="wLr-0x-V9T"/>
83104
</constraints>
84105
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
85106
<connections>
107+
<outlet property="instructionsLabel" destination="RlH-JE-Uvl" id="x8f-ZT-6fL"/>
86108
<outlet property="keyBindingDisplayLabel" destination="PoS-C4-kmO" id="38H-Et-LvV"/>
109+
<outlet property="saveButton" destination="zhT-CX-EI5" id="Vy5-p6-S3X"/>
110+
<outlet property="unassignButton" destination="Gb8-qr-Wmv" id="177-c8-Hma"/>
111+
<outlet property="unassignedLabel" destination="Ggp-Ki-OxV" id="QdS-Td-7gB"/>
87112
</connections>
88113
<point key="canvasLocation" x="34.5" y="54"/>
89114
</view>

KeyCommandKit/KeyBindingEditorViewController.swift

Lines changed: 80 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,11 @@ import UIKit
1010

1111
public class KeyBindingEditorViewController: UIViewController
1212
{
13-
@IBOutlet var _instructionsLabel: UILabel!
14-
1513
var binding: KeyBinding
1614

17-
var updatedBinding: KeyBinding?
15+
var result: EditorResult? = nil
1816

19-
var completion: ((KeyBinding?) -> Void)? = nil
17+
var completion: ((EditorResult?) -> Void)? = nil
2018

2119
var editorView: KeyBindingEditorView?
2220
{
@@ -26,7 +24,6 @@ public class KeyBindingEditorViewController: UIViewController
2624
init(binding: KeyBinding)
2725
{
2826
self.binding = binding
29-
self.updatedBinding = binding
3027
super.init(nibName: "KeyBindingEditorView", bundle: Bundle(for: KeyBindingEditorViewController.self))
3128

3229
self.preferredContentSize = CGSize(width: 275.0, height: 200.0)
@@ -42,7 +39,7 @@ public class KeyBindingEditorViewController: UIViewController
4239
super.viewDidLoad()
4340

4441
editorView?.viewController = self
45-
editorView?.keyBindingDisplayLabel.keyBinding = binding
42+
updateLabels()
4643

4744
navigationItem.title = ""
4845

@@ -68,10 +65,9 @@ public class KeyBindingEditorViewController: UIViewController
6865
input: keyCommand.input!, modifiers: keyCommand.modifierFlags,
6966
isDiscoverable: false)
7067

71-
self.editorView?.keyBindingDisplayLabel.keyBinding = binding
72-
// self.editorView?.keyBindingDisplayLabel.font = UIFont.systemFont(ofSize: 36.0)
68+
self.result = .customize(self.binding.customized(input: binding.input, modifiers: binding.modifiers))
7369

74-
self.updatedBinding = self.binding.customized(input: binding.input, modifiers: binding.modifiers)
70+
self.updateLabels()
7571
}
7672

7773
view.addSubview(keyBindingControl)
@@ -85,32 +81,90 @@ public class KeyBindingEditorViewController: UIViewController
8581

8682
DispatchQueue.main.async
8783
{
88-
self.completion?(self.updatedBinding)
84+
self.completion?(self.result)
8985
}
9086
}
9187

9288
public func setInstructions(_ text: String)
9389
{
94-
_instructionsLabel.text = text
90+
instructionsLabel.text = text
9591
}
9692

9793
public var instructionsLabel: UILabel
9894
{
99-
return _instructionsLabel
95+
return editorView!.instructionsLabel
96+
}
97+
98+
public var unassignedLabel: UILabel
99+
{
100+
return editorView!.unassignedLabel
101+
}
102+
103+
@objc func save()
104+
{
105+
dismiss(animated: true)
100106
}
101107

102108
@objc func revert()
103109
{
104-
self.updatedBinding = nil
110+
self.result = .revert
105111

106-
dismiss(animated: true, completion: nil)
112+
dismiss(animated: true)
107113
}
108114

109115
@objc func cancel()
110116
{
111-
self.updatedBinding = self.binding
117+
self.result = nil
112118

113-
dismiss(animated: true, completion: nil)
119+
dismiss(animated: true)
120+
}
121+
122+
@objc func unassign()
123+
{
124+
self.result = .unassign
125+
126+
dismiss(animated: true)
127+
}
128+
129+
enum EditorResult
130+
{
131+
case customize(KeyBinding)
132+
case revert
133+
case unassign
134+
}
135+
136+
// Private
137+
138+
private func updateLabels()
139+
{
140+
guard let editorView = self.editorView else
141+
{
142+
// If there's no editor view, there are no labels to update!
143+
return
144+
}
145+
146+
let binding: KeyBinding
147+
148+
// If there is a customized binding set, we read from there
149+
if case .some(.customize(let newBinding)) = result
150+
{
151+
binding = newBinding
152+
}
153+
else
154+
{
155+
binding = self.binding
156+
}
157+
158+
let isUnassignedBinding = binding.isUnassigned
159+
editorView.unassignedLabel.isHidden = !isUnassignedBinding
160+
editorView.keyBindingDisplayLabel.isHidden = isUnassignedBinding
161+
editorView.unassignButton.isEnabled = !isUnassignedBinding
162+
editorView.saveButton.isEnabled = result != nil
163+
164+
if !isUnassignedBinding
165+
{
166+
editorView.keyBindingDisplayLabel.keyBinding = binding
167+
}
114168
}
115169
}
116170

@@ -203,9 +257,18 @@ class KeyBindingEditorView: UIView
203257
var viewController: KeyBindingEditorViewController? = nil
204258

205259
@IBOutlet var keyBindingDisplayLabel: KeyBindingLabel!
260+
@IBOutlet var instructionsLabel: UILabel!
261+
@IBOutlet var unassignedLabel: UILabel!
262+
@IBOutlet var saveButton: UIButton!
263+
@IBOutlet var unassignButton: UIButton!
206264

207265
@IBAction func save(sender: Any?)
208266
{
209-
viewController?.dismiss(animated: true, completion: nil)
267+
viewController?.save()
268+
}
269+
270+
@IBAction func unassign(_ sender: Any?)
271+
{
272+
viewController?.unassign()
210273
}
211274
}

KeyCommandKit/KeyBindingsRegistry.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,12 @@ internal extension KeyBindingsRegistry
238238
return providersSortOrder[providerIndex]
239239
}
240240

241+
func customizeAsUnassigned(forKeyBinding binding: KeyBinding, inProviderWithIndex index: Int)
242+
{
243+
registerCustomization(input: UnassignedKeyBindingInput, modifiers: [],
244+
forKeyBinding: binding, inProviderWithIndex: index)
245+
}
246+
241247
/// Register a user-customizarion for a key binding.
242248
func registerCustomization(input: String, modifiers: UIKeyModifierFlags,
243249
forKeyBinding binding: KeyBinding,

KeyCommandKit/KeyBindingsViewController.swift

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,16 @@ open class KeyBindingsViewController: UITableViewController
5454
let binding = KeyBindingsRegistry.default.customization(forKeyBinding: original, inProviderWithIndex: indexPath.section)
5555

5656
keyBindingCell.titleLabel.text = binding.name
57-
keyBindingCell.keyBindingLabel.keyBinding = binding
57+
58+
if binding.isUnassigned
59+
{
60+
keyBindingCell.keyBindingLabel.isHidden = true
61+
}
62+
else
63+
{
64+
keyBindingCell.keyBindingLabel.keyBinding = binding
65+
keyBindingCell.keyBindingLabel.isHidden = false
66+
}
5867
}
5968

6069
cell.selectedBackgroundView = UIView()
@@ -91,21 +100,31 @@ open class KeyBindingsViewController: UITableViewController
91100

92101
editorViewController.completion =
93102
{
94-
newBinding in
103+
editorResult in
95104

96105
self.tableView.deselectRow(at: indexPath, animated: true)
97106

98-
if let newBinding = newBinding
107+
guard let editorResult = editorResult else
99108
{
100-
KeyBindingsRegistry.default.registerCustomization(input: newBinding.input,
101-
modifiers: newBinding.modifiers,
102-
forKeyBinding: binding,
103-
inProviderWithIndex: providerIndex)
109+
// Nil result means the user canceled the editor
110+
return
104111
}
105-
else
112+
113+
switch editorResult
106114
{
115+
case .customize(let newBinding):
116+
KeyBindingsRegistry.default.registerCustomization(input: newBinding.input,
117+
modifiers: newBinding.modifiers,
118+
forKeyBinding: binding,
119+
inProviderWithIndex: providerIndex)
120+
121+
case .revert:
107122
KeyBindingsRegistry.default.removeCustomization(forKeyBinding: binding,
108-
inProviderWithIndex: providerIndex)
123+
inProviderWithIndex: providerIndex)
124+
125+
case .unassign:
126+
KeyBindingsRegistry.default.customizeAsUnassigned(forKeyBinding: binding,
127+
inProviderWithIndex: providerIndex)
109128
}
110129

111130
self.tableView.reloadRows(at: [indexPath], with: .automatic)

0 commit comments

Comments
 (0)