I have implemented type selection for my NSCollectionView implementation.
In my collection view subclass, I have added additional delegate methods:
protocol CollectionViewDelegate : NSCollectionViewDelegate {
func collectionViewDelegateTypeSelect(_ text: String)
func collectionViewDelegateResetTypeSelect()
}
class CollectionView : NSCollectionView {
override func keyDown(with event: NSEvent) {
if let del = delegate as? CollectionViewDelegate {
if (event.characters?.rangeOfCharacter(from: CharacterSet.alphanumerics.inverted) == nil) || (event.characters == " ") {
del.collectionViewDelegateTypeSelect(event.characters!)
return
}
else {
del.collectionViewDelegateResetTypeSelect()
}
}
if (event.specialKey == NSEvent.SpecialKey.pageUp) {
scroll(NSMakePoint(0, max(0, enclosingScrollView!.documentVisibleRect.minY - enclosingScrollView!.documentVisibleRect.height)))
} else if (event.specialKey == NSEvent.SpecialKey.pageDown) {
scroll(NSMakePoint(0, min(enclosingScrollView!.documentVisibleRect.maxY, enclosingScrollView!.documentVisibleRect.origin.y + enclosingScrollView!.documentVisibleRect.height)))
} else if (event.specialKey == NSEvent.SpecialKey.home) {
scroll(NSMakePoint(0, 0))
} else if (event.specialKey == NSEvent.SpecialKey.end) {
scroll(NSMakePoint(0, frame.height))
} else {
super.keyDown(with: event)
}
}
}
Then in my delegate, which is a subclass of NSArrayController, I process the type selection:
var timer: Timer?
var typeString: String = ""
var typeCount: Int = 0
func collectionViewDelegateTypeSelect(_ text: String) {
typeCount = 0
typeString.append(text)
findItemClosestToString(typeString)
guard let _ = timer, timer!.isValid else {
timer = Timer(timeInterval: 0.1, repeats: true, block: {_ in
self.typeCount = self.typeCount + 1
if self.typeCount > 10 {
self.typeCount = 0
self.typeString = ""
self.timer?.invalidate()
}
})
RunLoop.main.add(timer!, forMode: RunLoop.Mode.default)
return
}
}
func collectionViewDelegateResetTypeSelect() {
if collectionViewDelegateIsTypeSelecting() {
timer?.invalidate()
timer = nil
}
}
func sortedItems() -> ([NSManagedObject], [String]) {
guard let items = (arrangedObjects as? [MediaItem]) else { return ([], []) }
let sorted = (items as NSArray).sortedArray(using: [alphabeticalSortDescriptor]) as! [MediaSeries]
return (sorted, items.map { $0.title })
}
func findItemClosestToString(_ text: String) {
let (items, strings) = sortedItems()
guard items.count > 0 else { return }
var lo = 0
var hi = items.count - 1
var mid = lo
while lo <= hi {
mid = (lo + hi)/2
let c = text.caseInsensitiveCompare(strings[mid])
if c == .orderedDescending {
lo = mid + 1
} else if c == .orderedAscending {
hi = mid - 1
} else {
break
}
}
if strings[mid].localizedLowercase.hasPrefix(text.localizedLowercase) == false, mid + 1 < strings.count, strings[mid + 1].localizedLowercase.hasPrefix(text.localizedLowercase) {
mid = mid + 1
}
selectItem(items, mid: mid)
}
func selectItem(_ items: [NSManagedObject], mid: Int) {
let index = (arrangedObjects as! [NSManagedObject]).firstIndex(of: items[mid])!
let path = IndexPath(item: index, section: 0)
mainCollection.deselectAll(nil)
mainCollection.reloadData()
mainCollection.selectItems(at: [path], scrollPosition: .top)
collectionView(mainCollection, didSelectItemsAt: [path])
}
This works pretty good, however, as the number of items the collection view displays grows (> 1000), it is possible to type faster than the type selection works, leaving the user to watch a few seconds of the UI catching up.
The selection functions are broken apart to make overriding individual parts easier in subclasses.
The question I have is how to best collate multiple NSEvents so that in the event a few Events are queued, only the character is added, so that the unnecessary mid-selections occur. About the only way I can think to do this is to add an additional timer that collects one or more events before running the main selection algorithm. Hence all that happens between user input, is the typeString
grows in length without proceeding further to doing the selection until I’m reasonably sure no additional input will come in.
When I started working through that, I figured there had to be a better approach. I know that NSTableViews and NSOutlineViews support type selection out of the box, but NSCollectionViews do not appear to.
What I’m curious about is if there is a better approach that I’ve just completely missed. I haven’t found anyone else attempting to add type selection to a NSCollectionView.