12

I recently reviewed one of my Applications that I released a year ago. And I see that nowadays the NSCollectionView inside it has lost the selection functioning such as SHIFT + Select now it behaving as CMD + Select.

(Secondary issue: I am also not getting a selection rectangle when dragging with the mouse.)

Obviously I want this feature back, where using shift would expand the selection from the previously clicked cell to the shift-clicked cell.

What have I done:

//NSCollectionView * _picturesGridView; //is my iVar

//In initialization I have set my _picturesGridView as follows
//Initializations etc are omitted -- (only the selection related code is here)
[_picturesGridView setSelectable:YES];
[_picturesGridView setAllowsMultipleSelection:YES];

Question: Is there an easy way to get back this functionality? I don't see anything related in documentation and I couldn't find any solution on the internet.

Sub Question: If there is no easy way to achieve that -> Should I go ahead and create my own FancyPrefix##CollectionViewClass and to reimplement this feature as I wish -- Or is it better to go over the existing NSCollectionView and force it to behave as I wish?

Sub Note: Well if I will find myself reimplementing it it will be light weight class that will just comply to my own needs -- I mean I will not mimic the entire NSCollectionView class.

P.S. I am able to select an item by clicking on it I am able to select multiple items only with CMD+Click or SHIFT+Click but the latter behaves exactly as CMD+Click which I don't want as well.

As for the mouse Selection Rectangle - I didn't override any Mouse events. It is not clear why I don't have this functionality.

Thomas Tempelmann
  • 11,045
  • 8
  • 74
  • 149
Coldsteel48
  • 3,482
  • 4
  • 26
  • 43
  • did you ever figure this one out, I just noticed that I cannot select multiple except one at a time... in my case the rectangle does work though, just not SHIFT-select – tofutim Nov 27 '17 at 22:26
  • Nope :-(, I rolled my own or subclasses something I don’t remeber, maybe I will take a look in to it tomorrow. – Coldsteel48 Nov 28 '17 at 04:23
  • 1
    I implemented my own subclass too. I checked where the selection first began and where the next item was selected with a shift-click, ignored default functionality and manually selected every index in the range. – Tritonal Nov 19 '20 at 07:16

3 Answers3

2

Per Apple's documentation, shouldSelectItemsAtIndexPaths: is the event that only gets invoked when the user interacts with the view, but not when the selection is modified by one's own code. Therefore, this approach avoids side effects that may occur when using didSelectItemsAt:.

It works as follows:

  1. It needs to remember the previously clicked-on item. This code assumes that the NSCollectionView is actually a subclass called CustomCollectionView that has a property @property (strong) NSIndexPath * _Nullable lastClickedIndexPath;
  2. It only remembers the last clicked-on item when it was a simple selection click, but not, for instance, a drag-selection with more than one selected item.
  3. If it detects a second single-selection click with the Shift key down, it selects all the items in the range from the last click to the current click.
  4. If an item gets deselected, we clear the last clicked-on item in order to simulate better the intended behavior.
- (NSSet<NSIndexPath *> *)collectionView:(CustomCollectionView *)collectionView shouldDeselectItemsAtIndexPaths:(NSSet<NSIndexPath*> *)indexPaths
{
    collectionView.lastClickedIndexPath = nil;
    return indexPaths;
}

- (NSSet<NSIndexPath *> *)collectionView:(CustomCollectionView *)collectionView shouldSelectItemsAtIndexPaths:(NSSet<NSIndexPath*> *)indexPaths
{
    if (indexPaths.count != 1) {
        // If it's not a single cell selection, then we also don't want to remember the last click position
        collectionView.lastClickedIndexPath = nil;
        return indexPaths;
    }
    NSIndexPath *clickedIndexPath = indexPaths.anyObject;   // now there is only one selected item
    NSIndexPath *prevIndexPath = collectionView.lastClickedIndexPath;
    collectionView.lastClickedIndexPath = clickedIndexPath; // remember last click
    BOOL shiftKeyDown = (NSEvent.modifierFlags & NSEventModifierFlagShift) != 0;
    if (NOT shiftKeyDown || prevIndexPath == nil) {
        // not an extension click
        return indexPaths;
    }
    NSMutableSet<NSIndexPath*> *newIndexPaths = [NSMutableSet set];
    NSInteger startIndex = [prevIndexPath indexAtPosition:1];
    NSInteger endIndex = [clickedIndexPath indexAtPosition:1];
    if (startIndex > endIndex) {
        // swap start and end position so that we can always count upwards
        NSInteger tmp = endIndex;
        endIndex = startIndex;
        startIndex = tmp;
    }
    for (NSInteger index = startIndex; index <= endIndex; ++index) {
        NSUInteger path[2] = {0, index};
        [newIndexPaths addObject:[NSIndexPath indexPathWithIndexes:path length:2]];
    }
    return newIndexPaths;
}

This answer has been made "Community wiki" so that anyone may improve this code. Please do so if you find a bug or can make it behave better.

Thomas Tempelmann
  • 11,045
  • 8
  • 74
  • 149
0

I hacked together a sample of doing this, starting with the sample code git@github.com:appcoda/PhotosApp.git, all my changes are in ViewController.swift.

I kept track of whether the shift was up or down by adding

var shiftDown = false
override func flagsChanged(with event: NSEvent) {
    shiftDown = ((event.modifierFlags.rawValue >> 17) & 1) != 0
}

I added this instance variable to remember the last selection

var lastIdx: IndexPath.Element?

Then, to the already defined collectionView(_:didSelectItemsAt), I added

let thisIdx = indexPaths.first?[1]
if shiftDown, let thisIdx = thisIdx, let lastIdx = lastIdx {
    let minItem = min(thisIdx, lastIdx)
    let maxItem = max(thisIdx, lastIdx)
    var additionalPaths = Set<IndexPath>()
    for i in minItem...maxItem {
        additionalPaths.insert(IndexPath(item: i, section: 0))
    }
    collectionView.selectItems(at: additionalPaths, scrollPosition: [])
}
lastIdx = thisIdx

This is just a start. There's probably bugs, especially around the saving of the lastIdx.

mr. fixit
  • 1,404
  • 11
  • 19
  • I believe it's not good practice to monitor the keyboard modifiers. Instead, you should probably get them from NSEvent inside the `didSelectItemsAt` handler. – Thomas Tempelmann Nov 22 '20 at 21:34
  • Also, instead of using `didSelectItemsAt:`, I believe it's better to use `shouldSelectItemsAtIndexPaths:` because that's the one that is made specifically for responding to *user interaction* and for modifying the user's selection before it's applied. – Thomas Tempelmann Nov 29 '20 at 16:00
0

None of the answers appear to support sections or shift selecting across section boundaries. Here is my approach:

- (NSSet<NSIndexPath *> *)collectionView:(NSCollectionView *)collectionView shouldSelectItemsAtIndexPaths:(NSSet<NSIndexPath *> *)indexPaths
{
    if ([shotCollectionView shiftIsDown])
    {
        // find the earliest and latest index path and make a set containing all inclusive indices
        NSMutableSet* inclusiveSet = [NSMutableSet new];
        
        __block NSIndexPath* earliestSelection = [NSIndexPath indexPathForItem:NSUIntegerMax inSection:NSUIntegerMax];
        __block NSIndexPath* latestSelection = [NSIndexPath indexPathForItem:0 inSection:0];
        
        [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath * _Nonnull obj, BOOL * _Nonnull stop) {
            
            NSComparisonResult earlyCompare = [obj compare:earliestSelection];

            NSComparisonResult latestCompare = [obj compare:latestSelection];

            if (earlyCompare == NSOrderedAscending)
            {
                earliestSelection = obj;
            }
            
            if (latestCompare == NSOrderedDescending)
            {
                latestSelection = obj;
            }
        }];
        
        NSSet<NSIndexPath *>* currentSelectionPaths = [shotCollectionView selectionIndexPaths];
        
        [currentSelectionPaths enumerateObjectsUsingBlock:^(NSIndexPath * _Nonnull obj, BOOL * _Nonnull stop) {
            
            NSComparisonResult earlyCompare = [obj compare:earliestSelection];

            NSComparisonResult latestCompare = [obj compare:latestSelection];

            if (earlyCompare == NSOrderedAscending)
            {
                earliestSelection = obj;
            }
            
            if (latestCompare == NSOrderedDescending)
            {
                latestSelection = obj;
            }
        }];
        
        NSUInteger earliestSection = [earliestSelection section];
        NSUInteger earliestItem = [earliestSelection item];
                                   
        NSUInteger latestSection = [latestSelection section];
        NSUInteger latestItem = [latestSelection item];
        
        for (NSUInteger section = earliestSection; section <= latestSection; section++)
        {
            NSUInteger sectionMin = (section == earliestSection) ? earliestItem : 0;
            NSUInteger sectionMax = (section == latestSection) ? latestItem : [self collectionView:collectionView numberOfItemsInSection:section];
            
            for (NSUInteger item = sectionMin; item <= sectionMax; item++)
            {
                NSIndexPath* path = [NSIndexPath indexPathForItem:item inSection:section];
                [inclusiveSet addObject:path];
            }
        }
        
        return inclusiveSet;
    }
    
    // Otherwise just pass through
    return indexPaths;
}
vade
  • 702
  • 4
  • 22