Inverting currently seleted items in NSTableView - macos

At the moment I can invert the currently selected items in an NSTableView with:
- (IBAction) doSelectInvert:(id) sender
{
NSIndexSet *set1=[myTable selectedRowIndexes];
NSMutableIndexSet *set2=[[NSMutableIndexSet alloc] init];
NSUInteger nCount=[myTable numberOfRows];
for(NSUInteger n=0;n<nCount;n++)
if(![set1 containsIndex:n]) [set2 addIndex:n];
[myTable selectRowIndexes:set2 byExtendingSelection:NO];
[set2 release];
}
Is this correct, or is there a more elegant way?

The most important improvement you can make is to adopt ARC. This will make your code more elegant by removing extra retain/release calls, and prevent a whole lot of simple errors. It's not fun to track down an extra release call that only sometimes has detectable symptoms.
Looking at all the NSMutableIndexSet methods, there are some more powerful operators, like -removeIndexes:, that can save you a bit of work, compared to building up your set one-at-a-time. Finding a way to use them sometimes means "inverting" your thinking a bit — for example, removing items from the set of everything, instead of adding items to an empty set.
- (IBAction) doSelectInvert:(id) sender
{
// start with all rows
NSRange allRows = NSMakeRange(0, [myTable numberOfRows]);
NSMutableIndexSet *invertedSelection = [NSMutableIndexSet indexSetWithIndexesInRange:allRows];
//then remove the currently selected rows
[invertedSelection removeIndexes:[myTable selectedRowIndexes]];
//set the new selection
[myTable selectRowIndexes:invertedSelection byExtendingSelection:NO];
}

Swift 5 variant, based on Vincent's answer and with feedback from #vadian:
var selection = IndexSet(integersIn: 0...tableView.numberOfRows)
selection.subtract(tableView.selectedRowIndexes)
tableView.selectRowIndexes(selection, byExtendingSelection: false)
Or if you prefer hard to read one liners:
tableView.selectRowIndexes(IndexSet(integersIn: 0...tableView.numberOfRows).subtracting(tableView.selectedRowIndexes), byExtendingSelection: false)

Related

NSTextView undo/redo attribute changes (when not first responder)

I'm building a basic text editor with custom controls. For my text alignment control, I need to cover two user scenarios:
the text view is the first responder - make the paragraph attribute changes to textView.rangesForUserParagraphAttributeChange
the text view is not the first responder - make the paragraph attribute changes to the full text range.
Here's the method:
- (IBAction)changedTextAlignment:(NSSegmentedControl *)sender
{
NSTextAlignment align;
// ....
NSRange fullRange = NSMakeRange(0, self.textView.textStorage.length);
NSArray *changeRanges = [self.textView rangesForUserParagraphAttributeChange];
if (![self.mainWindow.firstResponder isEqual:self.textView])
{
changeRanges = #[[NSValue valueWithRange:fullRange]];
}
[self.textView shouldChangeTextInRanges:changeRanges replacementStrings:nil];
[self.textView.textStorage beginEditing];
for (NSValue *r in changeRanges)
{
#try {
NSDictionary *attrs = [self.textView.textStorage attributesAtIndex:r.rangeValue.location effectiveRange:NULL];
NSMutableParagraphStyle *pStyle = [attrs[NSParagraphStyleAttributeName] mutableCopy];
if (!pStyle)
pStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
[pStyle setAlignment:align];
[self.textView.textStorage addAttributes:#{NSParagraphStyleAttributeName: pStyle}
range:r.rangeValue];
}
#catch (NSException *exception) {
NSLog(#"%#", exception);
}
}
[self.textView.textStorage endEditing];
[self.textView didChangeText];
// ....
NSMutableDictionary *typingAttrs = [self.textView.typingAttributes mutableCopy];
NSMutableParagraphStyle *pStyle = typingAttrs[NSParagraphStyleAttributeName];
if (!pStyle)
pStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
[pStyle setAlignment:align];
[typingAttrs setObject:NSParagraphStyleAttributeName forKey:pStyle];
self.textView.typingAttributes = typingAttrs;
}
So both scenarios work fine... BUT undo/redo doesn't work when the change is applied in the 'not-first-responder' scenario. The undo manager pushes something onto its stack (i.e Undo is available in the Edit menu), but invoking undo doesn't change the text. All it does is visibly select the full text range.
How do I appropriately change text view attributes so that undo/redo works regardless of whether the view is first reponder or not?
Thank you in advance!
I'm not sure, but I have two suggestions. One, check the return value from shouldChangeTextInRanges:..., since perhaps the text system is refusing your proposed change; a good idea in any case. Two, I would try to make the not-first-responder case more like the first-responder case in order to try to get it to work; in particular, you might begin by selecting the full range, so that rangesForUserParagraphAttributeChange is then in fact the range that you change the attributes on. A further step in this direction would be to actually momentarily make the textview be the first responder, for the duration of your change. In that case, the two cases should really be identical, I would think. You can restore the first responder as soon as you're done. Not optimal, but it seems that AppKit is making some assumption behind the scenes that you probably just have to work around. Without getting into trying to reproduce the problem and play with it, that's the best I can offer...
The issue is a typo on my part in the code that updates the typingAttributes afterwards. Look here:
//...
NSMutableParagraphStyle *pStyle = typingAttrs[NSParagraphStyleAttributeName];
// ...
Doh! Needs to be really mutable...
//...
NSMutableParagraphStyle *pStyle = [typingAttrs[NSParagraphStyleAttributeName] mutableCopy];
// ...

Iterate over NSTableview or NSArrayController to get data

I have an NSTableview which s bound to a NSArrayController. The Table/Arraycontroller contains Core Data "Person" entities. The people are added to the NSTableview by the GUI's user.
Let's say a person entity looks like
NSString* Name;
int Age;
NSString* HairColor;
Now I want to iterate over what is stored in the array controller to perform some operation in it. The actual operation I want to do isn't important I don't really want to get bogged down in what I am trying to do with the information. It's just iterating over everything held in the NSArraycontroller which is confusing me. I come from a C++ and C# background and am new to Cocoa. Let's say I want to build a NSMutableArray that contains each person from nsarraycontroller 1 year in the future.
So I would want to do something like
NSMutableArray* mutArray = [[NSMutableArray alloc] init];
foreach(PersonEntity p in myNsArrayController) // foreach doesn't exist in obj-c
{
Person* new_person = [[Person alloc] init];
[new_person setName:p.name];
[new_person setHairColor:p.HairColor];
[new_person setAge:(p.age + 1)];
[mutArray addObject:new_person];
}
I believe the only thing holding me back from doing something like the code above is that foreach does not exist in Obj-c. I just don't see how to iterate over the nsarraycontroller.
Note: This is for OSX so I have garbage collection turned on
You're looking for fast enumeration.
For your example, something like
for (PersonEntity *p in myNsArrayController.arrangedObjects)
{
// Rest of your code
}
You can also enumerate using blocks. For example:
[myNsArrayController enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop)
{
PersonEntity *p = object;
// Rest of your code
}];
There's pro's and cons to both approaches. These are discussed in depth in the answer to this question:
Objective-C enumerateUsingBlock vs fast enumeration?
You can find a great tutorial on blocks in Apple's WWDC 2010 videos. In that they say that at Apple they use blocks "all the time".

View Based Table Cells on OS X not showing data properly

So I admit to being a total noob to cocoa, so I offer a noob question. I'm probably just missing the dumb obvious somewhere but i just cant seem to get my table to populate data.
I'm following the table view playground example but everytime i try to mimic the Basic TableView Window the first row becomes the height of the number of rows i added (at least thats what it looks like. Here is my code:
- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(int)row
{
NSString *identifier = [tableColumn identifier];
if ([identifier isEqualToString:#"filename"]) {
// We pass us as the owner so we can setup target/actions into this main controller object
NSTableCellView *cellView = [fileBrowserTable makeViewWithIdentifier:identifier owner:self];
// Then setup properties on the cellView based on the column
cellView.textField.stringValue = [fileList filenameAtIndex:row];
cellView.imageView.objectValue = [[NSWorkspace sharedWorkspace] iconForFile:[fileList fullPathAtIndex:row]];
return cellView;
}
else if ([identifier isEqualToString:#"path"]) {
NSTextField *textField = [fileBrowserTable makeViewWithIdentifier:identifier owner:self];
textField.objectValue = [fileList pathAtIndex:row];
return textField;
}
else if ([identifier isEqualToString:#"preview"]) {
NSTextField *textField = [fileBrowserTable makeViewWithIdentifier:identifier owner:self];
textField.objectValue = [fileList previewAtIndex:row];
return textField;
}
return nil;
}
I think its worth mentioning that when using a the old school text field cell, I have no problems displaying data (of course the above code is different in that case) so im positive sure its not a problem with my data structure that holds the values. I have also set the correct delegate and data source
The cell using the 'filename' identifier uses the 'image and text table view cell' while the others use just a 'text table cell view'. Neither of them work so i'm guessing something is wrong with how I set my table up. But when comparing my table with that of the example, it's just a spitting reflection (minus identifiers file names).
One thing that I notice that I can't quite figure out is that the example says:
The NSTableView has two reuse identifier assocations: "MainCell" and "SizeCell" are both associated with the nib ATBasicTableViewCells.xib
I don't really understand this statement. However that being said, the example doesn't contain any ATBasicTableViewCells.xib nor does it have any associations with it (code or ib) that I can find.
Have you tried to set the rowSizeStyle of the NSTableView to NSTableViewRowSizeStyleCustom?
[UPDATE] Re-reading your question, it's not clear for me what your problem is. The solution I have given is related to problems with the size of each cell which is not taken into account unless the rowSizeStyle is set to custom.

How to use NSIndexSet

In Objective-C, my program opens a window and displays a table. I want to have a specified row of the table highlighted.
How do I do this?
I seem to need the code
[myTableView selectRowIndexes:(NSIndexSet *) byExtendingSelection:(BOOL)];
I looked at the developer documentation, and figured out that the BOOL should be NO.
By looking at the NSIndexSet docs, I can't figure out what the right syntax should be.
it would be the proper way:
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 3)];
or you can use the NSMutableIndexSet for the random indexes:
NSMutableIndexSet *mutableIndexSet = [[NSMutableIndexSet alloc] init];
[mutableIndexSet addIndex:0];
[mutableIndexSet addIndex:2];
[mutableIndexSet addIndex:9];
etc.
Printing out an NSIndexSet in the debugger will show you that they are internally NSRanges. To create one, you can either specify the range or a single explicit index (from which it will create the range); something like
NSIndexSet *indexes = [[NSIndexSet alloc] initWithIndex:rowToHighlight];
[myTableView selectRowIndexes:indexes byExtendingSelection:NO];
[indexes release];
Note that the index(es) must all be unsigned integers (NSUIntegers, specifically).
I'd use a factory method to avoid having to manage memory:
[myTableView selectRowIndexes:[NSIndexSet indexSetWithIndex:indexes]
byExtendingSelection:NO];
I seem to need the code
[myTableView selectRowIndexes:(NSIndexSet *) byExtendingSelection:(BOOL)];
No; those are casts without anything to cast, which is invalid.
Remove the casts and put values there instead.
I looked at the developer documentation, and figured out that the BOOL should be NO.
Yes, because you don't want to extend the selection, you want to replace it.
By looking at the NSIndexSet docs, I can't figure out what the right syntax should be.
The same as for passing any other variable or message expression.
You need to create an index set and then either stash it in a variable and pass that or pass the result of the creation message directly.

Renaming keys in NSMutableDictionary

Given an NSMutableDictionary *dict, is this a bad way to replace keys with a new name? Is there an easier way?
NSArray *originalField = [NSArray arrayWithObjects:#"oldkey", #"oldkey2", nil];
NSArray *replacedField = [NSArray arrayWithObjects:#"newkey", #"newkey2", nil];
for (int i=0; i<[originalField count]; ++i)
{
if ([dict objectForKey:[originalField objectAtIndex:i]] != nil) {
[dict setObject:[dict objectForKey:[originalField objectAtIndex:i]] forKey:[replacedField objectAtIndex:i]];
[dict removeObjectForKey:[originalField objectAtIndex:i]];
}
}
Thanks!
Nope, that's pretty much it. In general, you'd use fast enumeration and/or NSEnumerator to walk the arrays instead of going index-by-index, but since you're walking two parallel arrays, indexes are the clearest way to do it.
That's not a bad way per se, but you could certainly make it more elegant (and in my opinion, easier) by cleaning up the code a bit and eliminating a few redundant method calls. As #Peter suggested, fast enumeration (you can use it on Leopard+ or iPhone) would be much quicker and cleaner, and therefore generally preferable. Here's an example:
NSArray *originalField = [NSArray arrayWithObjects:#"oldkey", #"oldkey2", nil];
NSArray *replacedField = [NSArray arrayWithObjects:#"newkey", #"newkey2", nil];
id anObject;
NSEnumerator *replacementKeys = [replacedField objectEnumerator];
for (id originalKey in originalField) {
if ((anObject = [dict objectForKey:originalKey]) != nil) {
[dict removeObjectForKey:originalKey];
[dict setObject:anObject forKey:[replacementKeys nextObject]];
}
}
One note of warning: you'll want to make sure that the arrays originalField and replacedField are the same length. If the latter is shorter, you'll get an exception, either from -[NSEnumerator nextObject] or -[NSArray objectAtIndex:]. If the latter is longer, you may wonder why some of the replacement keys are never used. You could use an NSAssert macro to verify that during debugging, and it will be disabled automatically in release builds.
Alternatively, if there is truly a one-to-one relationship between the keys, perhaps you could use a dictionary to map from old key to new key, and enumerate over the result of -[NSDictionary allKeys].

Resources