I'm able to get my NSBrowser instance to display the correct data in the first column. When I select one of the options, however, the next column simply displays the same set of options. I have read the docs, looked at all of Apple's relevant sample code, and just about everything I could find on the internet but I simply can't figure out the correct way to implement the required methods. The data I'm supplying to the browser is an array of dictionaries. Each dictionary in turn contains a "children" key that is another array of dictionaries. And those dictionaries have their own "children" key that are also arrays of dictionaries, etc. Using JSON for descriptive purposes (objects are dictionaries, arrays are arrays), it looks like this:
data = [
{
name: 'David',
children:[
{
name: 'Sarah',
children: {...}
},
{
name: 'Kevin',
children: {...}
}
]
},
{
name: 'Mary',
children:[
{
name: 'Greg',
children: {...}
},
{
name: 'Jane',
children: {...}
}
]
}
]
So the first column should show "David" and "Mary". If "David" is selected, the next column should show "Sarah" and "Kevin", and so on.
My current implementation relies on a custom method I created that is supposed to translate the browser's index path into the corresponding NSArray level from the provided data. This method looks like:
- (NSArray *)getSelectionInBrowser:(NSBrowser *)browser
{
NSArray *selection = browserData;
NSIndexPath *path = [browser selectionIndexPath];
for (NSUInteger i = 0; i < path.length; i++) {
selection = [[selection objectAtIndex:i] objectForKey:#"children"];
}
return selection;
}
My implementation of the required NSBrowserDelegate protocol methods looks like:
- (NSInteger)browser:(NSBrowser *)sender numberOfRowsInColumn:(NSInteger)column
{
return [[self getSelectionInBrowser:sender] count];
}
- (NSInteger)browser:(NSBrowser *)browser numberOfChildrenOfItem:(id)item {
return [[self getSelectionInBrowser:browser] count];
}
- (id)browser:(NSBrowser *)browser child:(NSInteger)index ofItem:(id)item {
return [self getSelectionInBrowser:browser];
}
- (BOOL)browser:(NSBrowser *)browser isLeafItem:(id)item {
return ![item isKindOfClass:[NSMutableArray class]];
}
- (id)browser:(NSBrowser *)browser objectValueForItem:(id)item {
return nil;
}
- (void)browser:(NSBrowser *)browser willDisplayCell:(NSBrowserCell *)cell atRow:(NSInteger)row column:(NSInteger)column {
NSArray *selection = [self getSelectionInBrowser:browser];
cell.title = [[selection objectAtIndex:row] objectForKey:#"name"];
}
The first column of the NSBrowser is populated with the correct names. However, as soon as I make a selection the program crashes with the error -[__NSArrayM objectAtIndex:]: index 4 beyond bounds [0 .. 0]. After doing some debugging, the line of code it crashes on is the objectAtIndex: call in my custom getSelectionInBrowser:.
That doesn't fully surprise me because even before the crash I figured I was doing something wrong by relying on that custom method to retrieve the current selection. I imagine this work should be done within the delegate methods themselves and, when implemented correctly, the current selection should be accessible in the item variable that is provided in many of those methods. However, I couldn't get that to work. The item variable always seemed to be simply the root data object rather than reflecting the most "drilled-down" selection.
So how do I correct my implementation?
Solved it! Here is my final working code. No need for that custom getSelection... method, and a couple of the delegate methods I had were unnecessary (only used of you are NOT going with the "item-based API").
- (NSInteger)browser:(NSBrowser *)browser numberOfChildrenOfItem:(id)item {
if (item) {
return [[item objectForKey:#"children"] count];
}
return [browserData count];
}
- (id)browser:(NSBrowser *)browser child:(NSInteger)index ofItem:(id)item {
if (item) {
return [[item objectForKey:#"children"] objectAtIndex:index];
}
return [browserData objectAtIndex:index];
}
- (BOOL)browser:(NSBrowser *)browser isLeafItem:(id)item {
return [item objectForKey:#"children"] == nil;
}
- (id)browser:(NSBrowser *)browser objectValueForItem:(id)item {
return [item objectForKey:#"name"];
}
The first method is how you tell the NSBrowser the number of rows there should be. The second method is where you determine what data should be represented in a given row (index). In both cases, you must first check to see if item actually exists. If it doesn't, that's because you are at the root of the data (first column in the NSBrowser). Only when a row (or item!) in the NSBrowser gets selected will the item variable hold anything. The final method should return the string you wish to show in the given row.
Hopefully this helps people in the future.
Related
With file access in a sandboxed osx app with swift in mind, does it work the same with URLs provided via Finder or other apps drops?
As there's no NSOpenPanel call to afford folder access as in this example, just urls - I think the folder access is implicit since the user dragged the file from the source / desktop "folder" much the same as implicit selection via the open dialog.
I have not begun the sandbox migration yet but wanted to verify my thinking was accurate, but here's a candidate routine that does not work in sandbox mode:
func performDragOperation(_ sender: NSDraggingInfo!) -> Bool {
let pboard = sender.draggingPasteboard()
let items = pboard.pasteboardItems
if (pboard.types?.contains(NSURLPboardType))! {
for item in items! {
if let urlString = item.string(forType: kUTTypeURL as String) {
self.webViewController.loadURL(text: urlString)
}
else
if let urlString = item.string(forType: kUTTypeFileURL as String/*"public.file-url"*/) {
let fileURL = NSURL.init(string: urlString)?.filePathURL
self.webViewController.loadURL(url: fileURL!)
}
else
{
Swift.print("items has \(item.types)")
}
}
}
else
if (pboard.types?.contains(NSPasteboardURLReadingFileURLsOnlyKey))! {
Swift.print("we have NSPasteboardURLReadingFileURLsOnlyKey")
}
return true
}
as no URL is acted upon or error thrown.
Yes, the file access is implicit. As the sandbox implementation is poorly documented and had/has many bugs, you want to work around URL and Filenames. The view should register itself for both types at initialisation. Code is in Objective-C, but API should be the same.
[self registerForDraggedTypes:[NSArray arrayWithObjects:NSFilenamesPboardType, NSURLPboardType, nil]];
Then on performDragOperation:
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
BOOL dragPerformed = NO;
NSPasteboard *paste = [sender draggingPasteboard];
NSArray *typesWeRead = [NSArray arrayWithObjects:NSFilenamesPboardType, NSURLPboardType, nil];
//a list of types that we can accept
NSString *typeInPasteboard = [paste availableTypeFromArray:typesWeRead];
if ([typeInPasteboard isEqualToString:NSFilenamesPboardType]) {
NSArray *fileArray = [paste propertyListForType:#"NSFilenamesPboardType"];
//be careful since this method returns id.
//We just happen to know that it will be an array. and it contains strings.
NSMutableArray *urlArray = [NSMutableArray arrayWithCapacity:[fileArray count]];
for (NSString *path in fileArray) {
[urlArray addObject:[NSURL fileURLWithPath:path]];
}
dragPerformed = //.... do your stuff with the files;
} else if ([typeInPasteboard isEqualToString:NSURLPboardType]) {
NSURL *droppedURL = [NSURL URLFromPasteboard:paste];
if ([droppedURL isFileURL]) {
dragPerformed = //.... do your stuff with the files;
}
}
return dragPerformed;
}
The method itemWithTitle locates a menu item within a NSMenu. However it looks only inside the first level. I cannot find a ready-to-use method that will do the same job recursively by searching inside all the nested submenus. Or, somehow equivalently, a function that swipes NSmenu's recursively.
It looks quite incredible to me that such a thing would not exist. Maybe there is some function not directly related to NSMenu that can come in handy?
Ok, since I really need to do this clumsy workaround for a number of specific reasons (beyond the scope of this post, and quite uninteresting anyway) I ended up coding it myself.
Here is the snippet:
NSMenuItem * mitem;
while (mitem) { // loop over all menu items contained in any submenu, subsubmenu, etc.
// do something with mitem ....
mitem = next_menu_item(mitem);
}
which is powered by the functions:
NSMenuItem * goto_submenu(NSMenuItem * mitem){
NSMenu * submen = [mitem submenu];
if (submen && [[submen title] isNotEqualTo:#""] && [submen numberOfItems])
return goto_submenu([submen itemAtIndex:0]);
return mitem;
};
NSMenuItem * next_menu_item(NSMenuItem * mitem){
NSMenu * menu = [mitem menu];
if ([menu indexOfItem:mitem]==([menu numberOfItems]-1)) //if is last item in submenu go to parent item
return [mitem parentItem];
return goto_submenu([menu itemAtIndex:([menu indexOfItem:mitem]+1)]);
};
#implementation NSMenu (UT)
- (NSMenuItem *)itemWithTitle:(NSString *)title recursive:(BOOL)recursive {
if (recursive) {
for (NSMenuItem *item in self.itemArray) {
if ([item.title isEqualToString:title]) {
return item;
} else if (item.submenu) {
NSMenuItem *result = [item.submenu itemWithTitle:title recursive:recursive];
if (result) {
return result;
}
}
}
return nil;
} else {
return [self itemWithTitle:title];
}
}
- (NSMenuItem *)itemWithTag:(NSInteger)tag recursive:(BOOL)recursive {
if (recursive) {
for (NSMenuItem *item in self.itemArray) {
if (item.tag == tag) {
return item;
} else if (item.submenu) {
NSMenuItem *result = [item.submenu itemWithTag:tag recursive:recursive];
if (result) {
return result;
}
}
}
return nil;
} else {
return [self itemWithTag:tag];
}
}
#end
I am having a very hard time getting copy/paste to work on my custom managed object Person. The object contains properties and relationships. The object should provide the objectID. I intend to implement pasting generating a new object and then filling in the information from the copied Person.
Copying the objectID probably does work. I am certain that pasting does not work. I have the following methods implemented in my Person class, in an attempt to copy/paste an object:
#pragma mark --- Copy functionality
-(id)pasteboardPropertyListForType:(NSString *)type
{
if ( [type isEqualToString:#"my.company.person"])
{
NSManagedObjectID *oid = self.objectID;
NSURL *uidURL = [oid URIRepresentation];
return [uidURL absoluteString];
}
return nil;
}
-(NSArray *)writableTypesForPasteboard:(NSPasteboard *)pasteboard
{
return #[#"my.company.person"];
}
+ (NSPasteboardWritingOptions)writingOptionsForType:(NSString *)type pasteboard:(NSPasteboard *)pasteboard
{
if ( [type isEqualToString:#"my.company.person"])
{
return NSPasteboardWritingPromised;
}
return nil;
}
and to do the pasting:
#pragma mark --- Paste functionality
+(NSArray *)readableTypesForPasteboard:(NSPasteboard *)pasteboard
{
return #[#"my.company.person"];
}
+ (NSPasteboardReadingOptions)readingOptionsForType:(NSString *)type pasteboard:(NSPasteboard *)pasteboard
{
if ( [type isEqualToString:#"my.company.person"])
{
return NSPasteboardReadingAsString;
}
return nil;
}
- (id)initWithPasteboardPropertyList:(id)propertyList ofType:(NSString *)type
{
if ( [type isEqualToString:#"my.company.person"])
{
...
}
return nil;
}
How should I proceed here? I am at a loss, reading many stackoverflow Q&A's (e.g. Peter Hosey's great answer to NSPasteboard and simple custom data), as well as the Apple docs, still have me stumped on this one.
Have you already seen this:
Using Managed Objects?
I'm not sure what you mean about 'I need to return a Person object but I don't have a managed object context.' Your context is your scratch pad to create things.
I have an entity Tag (in core data) that have some attributes and a to-many relationship named "aliases".
My ArrayController is "bind" in Xcode to:
Parameters->Managed Object Context = File's Owner.managedObjectContext
It work OK for all Attributes, the others columns present the correct values.
In one column I try to "display" this to-many relationship. If I do it naively and bind the Value of my NSTableColumn to my ArrayController.arrangedObjects.aliases (like all other attributes) I get this on the screen:
Relationship fault for (),
name aliases, isOptional 1, isTransient 0, entity Tag,
renamingIdentifier aliases, validation predicates ( ), warnings ( ),
versionHashModifier (null), destination entity TagAlias,
inverseRelationship tag, minCount 0, maxCount 0 on 0x10053db10
It seems to be some kind of CoreData proxy for the relationship...
I then build a subclass of NSValueTransformer:
#interface tagAliasesToStringTransformer : NSValueTransformer
+ (Class)transformedValueClass;
+ (BOOL)allowsReverseTransformation;
- (id)transformedValue:(id)value;
#end
and tried to use it as a Value Transformer in the binding. But I'm lost in my implementation:
#implementation tagAliasesToStringTransformer
+ (Class)transformedValueClass {
return [NSString class];
}
+ (BOOL)allowsReverseTransformation {
return NO;
}
- (id)transformedValue:(id)value {
if (value == nil) return nil;
...
}
#end
In the method transformedValue:, value is of class '_NSFaultingMutableSet' and I don't know how to get a Set/Array of the aliases or anything useful.
The goal is to build a NSString of the concatenation of each alias. Something like:
aTag : alias1 alias2 alias3 ...
I found the solution:
_NSFaultingMutableSet is actually a kind of NSSet, so by doing something like this:
- (id)transformedValue:(id)value {
if (value == nil) return nil;
NSArray *tags = [value allObjects];
for (Tag *tag in tags) {
...
}
}
you get access to all the entity.
Don't know why it's obvious now and not one week ago... getting out of newbie zone ?
What is an easy way to set up my NSTableView with multiple columns to only display certain data in one column. I have the IBOutlets set up, but I don't know where to go from there.
Assuming you're not using Cocoa Bindings/Core Data, you can display data in an NSTableView by implementing two methods from the NSTableViewDataSource protocol. Typically your controller will implement the protocol, so open the controller .m file and add these methods to the controller's #implementation:
- (NSInteger)numberOfRowsInTableView:(NSTableView*)tableView {
return 25; // fill this out
}
– (id) tableView:(NSTableView*)tableView
objectValueForTableColumn:(NSTableColumn*)column
row:(int)row {
return row % 3 ? #"Tick..." : #"BOOM!"; // fill this out
}
You need to set the table's dataSource property to the controller. In Interface Builder control-drag from the table view to the controller and set dataSource. Now build and run and you should see your data in the table.
If you only want to fill out one column, add an IBOutlet NSTableColumn* to your controller; let's call it explosiveColumn. In Interface Builder, control-drag from the controller to the column you want to fill in and set explosiveColumn. Then, in tableView:objectValueForTableColumn:row: you can test if the column parameter is the same object as the one that the outlet is set to:
– (id) tableView:(NSTableView*)tableView
objectValueForTableColumn:(NSTableColumn*)column
row:(int)row {
if (column == explosiveColumn) {
return row % 3 ? #"Tick..." : #"BOOM!";
} else {
// other columns blank for now
return nil;
}
}
This tutorial might be useful: http://www.cocoadev.com/index.pl?NSTableViewTutorial
Here's an example using multiple table views with data source methods and a document based application:
#pragma mark - Data Source Methods
- (NSInteger) numberOfRowsInTableView:(NSTableView *)tv
{
if (tv == racerTableView)
return [racerList count];
else if (tv == vehicleTableView)
return [vehicleList count];
else
return 0; // something wrong here...
}
- (id)tableView:(NSTableView *)tv objectValueForTableColumn:(NSTableColumn *)col
row:(NSInteger)rowi
{
NSString *colid = [col identifier];
if (tv == racerTableView){
NHRacers *racer = [racerList objectAtIndex:rowi];
return [racer valueForKey:colid];
}
else if (tv == vehicleTableView){
NHVehicles *vehicle = [vehicleList objectAtIndex:rowi];
return [vehicle valueForKey:colid];
}
else
return 0; // something wrong here...
}
- (void)tableView:(NSTableView *)tv setObjectValue:(id)obj forTableColumn:(NSTableColumn *)col row:(NSInteger)rowi
{
NSString *colid = [col identifier];
if (tv == racerTableView) {
NHRacers *racer = [racerList objectAtIndex:rowi];
[racer setValue:obj forKey:colid];
}
else if (tv == vehicleTableView){
NHVehicles *vehicle = [vehicleList objectAtIndex:rowi];
[vehicle setValue:obj forKey:colid];
}
else
nil; // something wrong here...
[self updateChangeCount:NSChangeDone];
}
The tableview datasource outlets are set to the File's Owner and the File's Owner has set vehicleTableView and racerTableView to their respective "Table View" in the IB. The colid key checks the identifier (set in IB by selecting the table view column under the "Identity" drop down, while the "Identity Inspector" is shown). These values were chosen to be the KVC (key coding compliant) properties of the classes being displayed in the table views: use lower case first letter (see apple docs for rest).
For example:
(in NHVehicles.h)
#interface NHVehicles : NSObject
{
NSUInteger entry;
NSString *name;
NSString *vehicleClass;
}
#property NSUInteger entry;
#property NSString *name, *vehicleClass;
#end
(in NHVehicles.m)
#implementation NHVehicles
#synthesize entry, name, vehicleClass;
#end
for this tableView, "entry", "name" and "vehicleClass" would be typed (w/o ") into the identifier fields for their respective columns.
If you don't want to show some data in the class, simply do not enter the key for the column identifier. A word of caution: I am using Xcode 4.5.1 and I noticed that once I had entered a few keys for a particular column identifiers and then changed my mind about and attempted to clear the text, it complained when I deleted the text from the identifier field (I could no longer leave the field blank for the columns that I had edited). This was not difficult to work around, but it was a surprise.