Skip to content

Commit ab187dd

Browse files
authored
Merge pull request openvanilla#663 from zonble/master
Enhance accessibility in the candidate window
2 parents eb161d1 + ee03ac1 commit ab187dd

15 files changed

Lines changed: 403 additions & 24 deletions

McBopomofo.xcodeproj/project.pbxproj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
D43737C92DF9C35800D9707C /* InputMethodController+KeyHandlerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43737C82DF9C35100D9707C /* InputMethodController+KeyHandlerDelegate.swift */; };
5656
D43737CB2DF9C48300D9707C /* InputMethodController+CandidateControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43737CA2DF9C48200D9707C /* InputMethodController+CandidateControllerDelegate.swift */; };
5757
D43FC40B2B23788400ED5A1C /* InputMacro.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43FC40A2B23788400ED5A1C /* InputMacro.swift */; };
58+
D4451AC92E688C6B00E8F5AB /* SystemCharacterInfo in Frameworks */ = {isa = PBXBuildFile; productRef = D4451AC82E688C6B00E8F5AB /* SystemCharacterInfo */; };
5859
D449AD5F2B393C00000C5812 /* InputMacroTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D449AD5E2B393C00000C5812 /* InputMacroTests.swift */; };
5960
D449AD612B39506D000C5812 /* ServiceProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D449AD602B39506D000C5812 /* ServiceProviderTests.swift */; };
6061
D44FB74527915565003C80A6 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44FB74427915555003C80A6 /* Preferences.swift */; };
@@ -229,6 +230,7 @@
229230
D4E33D9127A838F7006DB1CF /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
230231
D4E569DA27A34CC100AC2CEF /* KeyHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyHandler.h; sourceTree = "<group>"; };
231232
D4E569DB27A34CC100AC2CEF /* KeyHandler.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyHandler.mm; sourceTree = "<group>"; };
233+
D4E5EEE22E68A9B30068BC58 /* SystemCharacterInfo */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SystemCharacterInfo; path = Packages/SystemCharacterInfo; sourceTree = "<group>"; };
232234
D4E791782B52CDCF00676A68 /* ChineseNumbers */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = ChineseNumbers; path = Packages/ChineseNumbers; sourceTree = "<group>"; };
233235
D4EE67572B39685F00F062DE /* DictionaryServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryServiceTests.swift; sourceTree = "<group>"; };
234236
D4EE67592B39968900F062DE /* dictionary_service.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = dictionary_service.json; sourceTree = "<group>"; };
@@ -249,6 +251,7 @@
249251
D427F7B4279086DC004A2160 /* InputSourceHelper in Frameworks */,
250252
D427F7C127908EFC004A2160 /* OpenCCBridge in Frameworks */,
251253
D427F7A927905E90004A2160 /* TooltipUI in Frameworks */,
254+
D4451AC92E688C6B00E8F5AB /* SystemCharacterInfo in Frameworks */,
252255
D47D73C327A7200500255A50 /* FSEventStreamHelper in Frameworks */,
253256
D4E7917A2B52CDE500676A68 /* ChineseNumbers in Frameworks */,
254257
D427F76A278C9E29004A2160 /* CandidateUI in Frameworks */,
@@ -453,6 +456,7 @@
453456
D427F766278C9CBD004A2160 /* Packages */ = {
454457
isa = PBXGroup;
455458
children = (
459+
D4E5EEE22E68A9B30068BC58 /* SystemCharacterInfo */,
456460
D41B626A2B86EAD400583148 /* BopomofoBraille */,
457461
D4E791782B52CDCF00676A68 /* ChineseNumbers */,
458462
D427F768278C9D0D004A2160 /* CandidateUI */,
@@ -534,6 +538,7 @@
534538
D4C9CAB027AAC9690058DFEA /* NSStringUtils */,
535539
D4E791792B52CDE500676A68 /* ChineseNumbers */,
536540
D41B626B2B86EAE900583148 /* BopomofoBraille */,
541+
D4451AC82E688C6B00E8F5AB /* SystemCharacterInfo */,
537542
);
538543
productName = McBopomofo;
539544
productReference = 6A0D4EA215FC0D2D00ABF4B3 /* McBopomofo.app */;
@@ -1454,6 +1459,10 @@
14541459
isa = XCSwiftPackageProductDependency;
14551460
productName = OpenCCBridge;
14561461
};
1462+
D4451AC82E688C6B00E8F5AB /* SystemCharacterInfo */ = {
1463+
isa = XCSwiftPackageProductDependency;
1464+
productName = SystemCharacterInfo;
1465+
};
14571466
D47D73C227A7200500255A50 /* FSEventStreamHelper */ = {
14581467
isa = XCSwiftPackageProductDependency;
14591468
productName = FSEventStreamHelper;

Packages/CandidateUI/Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
// swift-tools-version:5.3
1+
// swift-tools-version:5.9
22
// The swift-tools-version declares the minimum version of Swift required to build this package.
33

44
import PackageDescription
55

66
let package = Package(
77
name: "CandidateUI",
8-
platforms: [.macOS(.v10_10)],
8+
platforms: [.macOS(.v10_15)],
99
products: [
1010
.library(
1111
name: "CandidateUI",

Packages/CandidateUI/Sources/CandidateUI/CandidateController.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ public class CandidateKeyLabel: NSObject {
3939
public protocol CandidateControllerDelegate: AnyObject {
4040
func candidateCountForController(_ controller: CandidateController) -> UInt
4141
func candidateController(_ controller: CandidateController, candidateAtIndex index: UInt) -> String
42+
func candidateController(_ controller: CandidateController, readingAtIndex index: UInt) -> String?
43+
func candidateController(_ controller: CandidateController, requestExplanationFor candidate: String, reading: String) -> String?
4244
func candidateController(_ controller: CandidateController, didSelectCandidateAtIndex index: UInt)
4345
}
4446

Packages/CandidateUI/Sources/CandidateUI/HorizontalCandidateController.swift

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,26 +23,40 @@
2323

2424
import Cocoa
2525

26+
private protocol HorizontalCandidateViewDelegate: AnyObject {
27+
func view(_ view: HorizontalCandidateView, didRequestExplanationFor candidate: String, reading: String) -> String?
28+
}
29+
2630
private class HorizontalCandidateView: NSView {
31+
32+
fileprivate weak var delegate: HorizontalCandidateViewDelegate?
33+
2734
fileprivate final class CandidateAXItem: NSAccessibilityElement {
2835
weak var owner: HorizontalCandidateView?
2936
let index: UInt
3037
let candidate: String
38+
let reading: String?
3139
let rect: NSRect
3240

3341
init(owner: HorizontalCandidateView, index: UInt,
34-
candidate: String,
35-
rect: NSRect)
42+
candidate: String, reading: String?, rect: NSRect)
3643
{
3744
self.owner = owner
3845
self.index = index
3946
self.candidate = candidate
47+
self.reading = reading
4048
self.rect = rect
4149
}
4250

4351
override func accessibilityParent() -> Any? { owner }
44-
override func accessibilityRole() -> NSAccessibility.Role { .staticText }
45-
override func accessibilityLabel() -> String? { candidate }
52+
override func accessibilityRole() -> NSAccessibility.Role { .unknown }
53+
override func accessibilityLabel() -> String? {
54+
guard let owner = owner, let delegate = owner.delegate, let reading else {
55+
return candidate
56+
}
57+
let explan = delegate.view(owner, didRequestExplanationFor: candidate, reading: reading)
58+
return explan ?? candidate
59+
}
4660
override func isAccessibilityElement() -> Bool { true }
4761

4862
func accessibilitySelected() -> Bool {
@@ -72,7 +86,7 @@ private class HorizontalCandidateView: NSView {
7286
weak var target: AnyObject?
7387

7488
private var keyLabels: [String] = []
75-
private var displayedCandidates: [String] = []
89+
private var displayedCandidates: [(candidate: String, reading: String?)] = []
7690
private var keyLabelHeight: CGFloat = 0
7791
private var candidateTextHeight: CGFloat = 0
7892
private var cellPadding: CGFloat = 0
@@ -118,7 +132,7 @@ private class HorizontalCandidateView: NSView {
118132
return result
119133
}
120134

121-
func set(keyLabels labels: [String], displayedCandidates candidates: [String]) {
135+
func set(keyLabels labels: [String], displayedCandidates candidates: [(candidate:String, reading: String?)]) {
122136
let count = min(labels.count, candidates.count)
123137
keyLabels = Array(labels[0 ..< count])
124138
displayedCandidates = Array(candidates[0 ..< count])
@@ -129,7 +143,7 @@ private class HorizontalCandidateView: NSView {
129143
let labelRect = (keyLabels[index] as NSString).boundingRect(
130144
with: baseSize, options: .usesLineFragmentOrigin, attributes: keyLabelAttrDict
131145
)
132-
let candidateRect = (displayedCandidates[index] as NSString).boundingRect(
146+
let candidateRect = (displayedCandidates[index].candidate as NSString).boundingRect(
133147
with: baseSize, options: .usesLineFragmentOrigin, attributes: candidateAttrDict
134148
)
135149
let cellWidth =
@@ -253,7 +267,7 @@ private class HorizontalCandidateView: NSView {
253267
}
254268
NSBezierPath.fill(candidateRect)
255269
}
256-
(displayedCandidates[index] as NSString).draw(
270+
(displayedCandidates[index].candidate as NSString).draw(
257271
in: candidateRect, withAttributes: activeCandidateAttr
258272
)
259273
accuWidth += currentWidth + 1.0
@@ -331,7 +345,8 @@ private class HorizontalCandidateView: NSView {
331345
let element = CandidateAXItem(
332346
owner: self,
333347
index: UInt(index),
334-
candidate: candidate,
348+
candidate: candidate.candidate,
349+
reading: candidate.reading,
335350
rect: rect
336351
)
337352
children.append(element)
@@ -439,6 +454,8 @@ public class HorizontalCandidateController: CandidateController {
439454

440455
prevPageButton.target = self
441456
prevPageButton.action = #selector(pageButtonAction(_:))
457+
458+
candidateView.delegate = self
442459
}
443460

444461
@available(*, unavailable)
@@ -536,6 +553,12 @@ public class HorizontalCandidateController: CandidateController {
536553
}
537554
}
538555

556+
extension HorizontalCandidateController: HorizontalCandidateViewDelegate {
557+
fileprivate func view(_ view: HorizontalCandidateView, didRequestExplanationFor candidate: String, reading: String) -> String? {
558+
delegate?.candidateController(self, requestExplanationFor: candidate, reading: reading)
559+
}
560+
}
561+
539562
extension HorizontalCandidateController {
540563
private var pageCount: UInt {
541564
guard let delegate = delegate else {
@@ -552,18 +575,17 @@ extension HorizontalCandidateController {
552575
}
553576

554577
candidateView.set(keyLabelFont: keyLabelFont, candidateFont: candidateFont)
555-
var candidates = [String]()
578+
var candidates = [(candidate: String, reading: String?)]()
556579
let count = delegate.candidateCountForController(self)
557580
let keyLabelCount = UInt(keyLabels.count)
558581

559582
let begin = currentPage * keyLabelCount
560583
for index in begin ..< min(begin + keyLabelCount, count) {
561584
let candidate = delegate.candidateController(self, candidateAtIndex: index)
562-
candidates.append(candidate)
585+
let reading = delegate.candidateController(self, readingAtIndex: index)
586+
candidates.append((candidate, reading))
563587
}
564-
candidateView.set(
565-
keyLabels: keyLabels.map { $0.displayedText }, displayedCandidates: candidates
566-
)
588+
candidateView.set(keyLabels: keyLabels.map { $0.displayedText }, displayedCandidates: candidates)
567589
candidateView.toolTip = tooltip
568590
var newSize = candidateView.sizeForView
569591
var frameRect = candidateView.frame

Packages/CandidateUI/Sources/CandidateUI/VerticalCandidateController.swift

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,26 +110,49 @@ private class BackgroundView: NSView {
110110
}
111111
}
112112

113+
private protocol VerticalCandidateTableViewDelegate: AnyObject {
114+
func view(_ view: VerticalCandidateTableView, didRequestExplanationFor candidate: String, reading: String) -> String?
115+
func view(_ view: VerticalCandidateTableView, readingAtIndex index: UInt) -> String?
116+
}
117+
113118
private class VerticalCandidateTableView: NSTableView {
119+
fileprivate weak var explanDelegate: VerticalCandidateTableViewDelegate?
120+
114121
fileprivate final class CandidateAXItem: NSAccessibilityElement {
115122
weak var owner: VerticalCandidateTableView?
116123
let index: UInt
117124
let candidate: NSAttributedString?
125+
let reading: String?
118126
let rect: NSRect
119127

120128
init(owner: VerticalCandidateTableView, index: UInt,
121129
candidate: NSAttributedString?,
130+
reading: String?,
122131
rect: NSRect)
123132
{
124133
self.owner = owner
125134
self.index = index
126135
self.candidate = candidate
136+
self.reading = reading
127137
self.rect = rect
128138
}
129139

130140
override func accessibilityParent() -> Any? { owner }
131-
override func accessibilityRole() -> NSAccessibility.Role { .staticText }
132-
override func accessibilityLabel() -> String? { candidate?.string }
141+
override func accessibilityRole() -> NSAccessibility.Role { .unknown }
142+
override func accessibilityLabel() -> String? {
143+
guard let owner = owner,
144+
let delegate = owner.explanDelegate,
145+
let candate = candidate?.string,
146+
let reading else {
147+
return candidate?.string
148+
}
149+
let explan = delegate.view(
150+
owner,
151+
didRequestExplanationFor: candate,
152+
reading: reading
153+
)
154+
return explan ?? candidate?.string
155+
}
133156
override func isAccessibilityElement() -> Bool { true }
134157

135158
func accessibilitySelected() -> Bool {
@@ -166,14 +189,21 @@ private class VerticalCandidateTableView: NSTableView {
166189
}
167190
var children = [CandidateAXItem]()
168191
for i in 0 ..< Int(rows) {
169-
let value = dataSource.tableView?(self, objectValueFor: nil, row: i)
192+
let candidate = dataSource.tableView?(self, objectValueFor: nil, row: i)
193+
let reading = explanDelegate?.view(self, readingAtIndex: UInt(i))
170194
let rect = NSRect(
171195
x: 0,
172196
y: CGFloat(i) * rowHeight,
173197
width: frame.width,
174198
height: rowHeight
175199
)
176-
let child = CandidateAXItem(owner: self, index: UInt(i), candidate: value as? NSAttributedString, rect: rect)
200+
let child = CandidateAXItem(
201+
owner: self,
202+
index: UInt(i),
203+
candidate: candidate as? NSAttributedString,
204+
reading: reading,
205+
rect: rect
206+
)
177207
children.append(child)
178208
}
179209
self.children = children
@@ -278,6 +308,7 @@ public class VerticalCandidateController: CandidateController {
278308
super.init(window: panel)
279309
tableView.dataSource = self
280310
tableView.delegate = self
311+
tableView.explanDelegate = self
281312
tableView.doubleAction = #selector(rowDoubleClicked(_:))
282313
tableView.target = self
283314

@@ -628,6 +659,21 @@ extension VerticalCandidateController: NSTableViewDataSource, NSTableViewDelegat
628659
}
629660
}
630661

662+
extension VerticalCandidateController: VerticalCandidateTableViewDelegate {
663+
fileprivate func view(_ view: VerticalCandidateTableView, didRequestExplanationFor candidate: String, reading: String) -> String? {
664+
delegate?
665+
.candidateController(
666+
self,
667+
requestExplanationFor: candidate,
668+
reading: reading
669+
)
670+
}
671+
672+
fileprivate func view(_ view: VerticalCandidateTableView, readingAtIndex index: UInt) -> String? {
673+
delegate?.candidateController(self, readingAtIndex: UInt(index))
674+
}
675+
}
676+
631677
extension NSImage {
632678
static func mask(withCornerRadius radius: CGFloat) -> NSImage {
633679
let image = NSImage(size: NSSize(width: radius * 2, height: radius * 2), flipped: false) {

Packages/CandidateUI/Tests/CandidateUITests/HorizontalCandidateControllerTests.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ final class HorizontalCandidateControllerTests {
2222
func candidateController(_ controller: CandidateController, didSelectCandidateAtIndex index: UInt) {
2323
selected = candidates[Int(index)]
2424
}
25+
26+
func candidateController(_ controller: CandidateController, readingAtIndex index: UInt) -> String? {
27+
nil
28+
}
29+
30+
func candidateController(_ controller: CandidateUI.CandidateController, requestExplanationFor candidate: String, reading: String) -> String? {
31+
nil
32+
}
2533
}
2634

2735
@Test("Test if candidate controller can be positioned correctly when the input position is below the bottom of screen")

Packages/CandidateUI/Tests/CandidateUITests/VerticalCandidateControllerTests.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ final class VerticalCandidateControllerTests {
2121
func candidateController(_ controller: CandidateController, didSelectCandidateAtIndex index: UInt) {
2222
selected = candidates[Int(index)]
2323
}
24+
25+
func candidateController(_ controller: CandidateController, readingAtIndex index: UInt) -> String? {
26+
nil
27+
}
28+
29+
func candidateController(_ controller: CandidateUI.CandidateController, requestExplanationFor candidate: String, reading: String) -> String? {
30+
nil
31+
}
2432
}
2533

2634
@Test("Test if candidate controller can be positioned correctly when the input position is below the bottom of screen")
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
xcuserdata/
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc

Packages/SystemCharacterInfo/Package.resolved

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)