Understanding the basics behind NSOutlineView

Hi all!

I’m having trouble wrapping my head around the basics of setting up an Outline View with parent and child items.

My goal is to take some of my Main Menu’s Submenu List Items, and turn them into a nested Outline View.

The list items are links to files within a bundle (bundle > file, file). Right now, if you click on a menu item it will open a text view where you can edit the linked file.

Rather than having the list in the Main Menu, I’d like to have an outline view in the same window as the text view. Sounds so easy! But, the concept of how to do it is making my head spin. I’ve got something similar with an NSTableView, but the items aren’t nested.

NSOutlineView looks to be a lot more confusing to set up, but the examples I’ve seen may have just been overly complicated or written before ASOC; adding to the confusion.

If anyone can help explain how to set up a simple Outline View from a list of items, I’d be forever grateful.

Thank you so much!

I’m not sure the terms “simple” and “Outline View” belong in the same sentence, to be honest. If your files are nested to a similar depth, you might do better with multiple tables that cascade content.

But the key to outline views, IMO, is setting up the original data suitably. If you’re using files, the Cocoa sample in the docs should give you what you need to get started. Otherwise you need to have your data in suitably nested records, or a list thereof, and then use a tree controller and bindings.

Thanks for the reply! So it’s not “just” me :wink: . Outline View sounds like something I should tackle when I’ve got a little more ObjC under my belt. Would you mind directing me to the Cocoa sample? I’ve found the programming guides, but the only sample I found was for a crazy “kitchen sink” kinda demo.

Thanks again!

Search the docs for “Writing an Outline View Data Source”.

mac.jedi,

Here is an example of how to create a simple outline view by manually adding nodes, and child nodes. I use the NSTreeNode method, treeNodeWithRepresentedObject:, to create nodes whose represented objects are just simple strings. At the top level, you just add these nodes to the array using addObject:, but for levels further down, you have to add the nodes to the parent node’s mutableChildNodes array. Here is the code:

script SimpleOutlineAppDelegate
	property parent : class "NSObject"
	property theData : missing value
	property NSTN : class "NSTreeNode"
	
	on applicationWillFinishLaunching_(aNotification)
		set theData to current application's NSMutableArray's array()
		
		-- Create top level nodes and add them to the base array
		set topNode1 to NSTN's treeNodeWithRepresentedObject_("Pennsylvania")
		set topNode2 to NSTN's treeNodeWithRepresentedObject_("Utah")
		set topNode3 to NSTN's treeNodeWithRepresentedObject_("California")
		theData's addObjectsFromArray_({topNode1, topNode2, topNode3})
		
		--Add child nodes to topNode1
		topNode1's mutableChildNodes()'s addObject_(NSTN's treeNodeWithRepresentedObject_("Philadelphia"))
		topNode1's mutableChildNodes()'s addObject_(NSTN's treeNodeWithRepresentedObject_("Pittsburgh"))
		topNode1's mutableChildNodes()'s addObject_(NSTN's treeNodeWithRepresentedObject_("Harrisburg"))
		
		--Add child nodes to topNode2
		topNode2's mutableChildNodes()'s addObject_(NSTN's treeNodeWithRepresentedObject_("Salt Lake City"))
		topNode2's mutableChildNodes()'s addObject_(NSTN's treeNodeWithRepresentedObject_("Provo"))
		
		--Add child nodes to topNode3
		topNode3's mutableChildNodes()'s addObject_(NSTN's treeNodeWithRepresentedObject_("Sacramento"))
		
		
		-- Add child nodes to the first child node of topNode 1
		set philly to topNode1's childNodes()'s objectAtIndex_(0)
		philly's mutableChildNodes()'s addObject_(NSTN's treeNodeWithRepresentedObject_("Schuylkill"))
		philly's mutableChildNodes()'s addObject_(NSTN's treeNodeWithRepresentedObject_("Delaware River"))
		
		setTheData_(theData)
	end applicationWillFinishLaunching_
	
end script

In IB, there are a few things to hook up. I added an NSOutlineView, and set it to have one column, and I added an NSTreeController object. The tree controller’s content array is bound to SimpleOutline App Delegate with the model key path of theData just as you would for a table view. In the attributes pane of the inspector, you have to add the word “childNodes” to the field labeled “children” under the Key Paths section. That’s it for the tree controller. The only other thing is to bind the outline view’s one column. Its value is bound to the tree controller with a Controller Key of arrangedObjects and a Model Key Path of representedObject.

That’s all there is to it for this simple example. I will shortly post a more useful example that populates an outline view with a file tree of a supplied folder path.

Ric

This example populates an outline view with a file tree, like you see in Finder (but with only one column). It is hooked up in IB just like the simple example I posted above (with one addition which I’ll mention in a bit). The applicationWillFinishLaunching method does a bunch of setup, and then calls the recursive method, addToNode_atPath_ . This method basically does the same thing as what my simple example did, but in a recursive way that goes down through all the subpaths, adding nodes as it goes. Here is the code:

script OutlineTestAppDelegate
	property parent : class "NSObject"
	property theData : missing value -- Array to contain the supplied folder's file tree
	property ov : missing value -- IBOutlet for the OutlineView
	property manager : missing value
	property pred : missing value
	
	on applicationWillFinishLaunching_(aNotification)
		ov's setDoubleAction_("processFile:")
		ov's setTarget_(me)
		set theTransformer to current application's FullPathToNameTransformer's alloc()'s init()
		current application's NSValueTransformer's setValueTransformer_forName_(theTransformer, "NameFromPathTransformer")
		set theData to current application's NSMutableArray's array()
		set manager to current application's NSFileManager's defaultManager()
		set pred to current application's NSPredicate's predicateWithFormat_("NOT SELF == %@", ".DS_Store")
		set thePath to current application's NSString's stringWithString_(POSIX path of ((path to desktop folder as string) & "FileParser:"))
		addToNode_atPath_(theData, thePath)
		setTheData_(theData)
	end applicationWillFinishLaunching_
	
	on addToNode_atPath_(currentNode, thePath)
		set theArray to manager's contentsOfDirectoryAtPath_error_(thePath, missing value)
		set theArray to theArray's filteredArrayUsingPredicate_(pred) -- Used to get rid of the ".DS_Store" files
		repeat with anItem in theArray
			set itemFullPath to thePath's stringByAppendingPathComponent_(anItem)
			set aNode to current application's NSTreeNode's treeNodeWithRepresentedObject_(itemFullPath)
			set theFileType to manager's attributesOfItemAtPath_error_(aNode's representedObject(), missing value)'s fileType()
			if theFileType is current application's NSFileTypeDirectory then
				addToNode_atPath_(aNode, aNode's representedObject())
			end if
			if currentNode is theData then
				currentNode's addObject_(aNode)
			else
				currentNode's mutableChildNodes()'s addObject_(aNode)
			end if
		end repeat
	end addToNode_atPath_
	
	on processFile_(sender)
		log sender's itemAtRow_(sender's selectedRow())'s representedObject()'s representedObject()
	end processFile_
	
end script

Two things to note here. Because it was easier (to my way of thinking anyway) and also more useful if you want to do something with the paths in the view, I made the representedObjects be full paths. If you do the binding for the column in IB just like I stated in the simple example above, you will see full paths in the outline view – this isn’t necessarily the best way to view the data, so I use a value transformer to show only the file or folder names – to do this, you just need to add “NameFromPathTransformer” in the Value Transformer field of the column’s binding. That name is defined in the 4th line of the applicationWillFinishLaunching method after the transformer is initialized in line 3. I find these transformers to be a nice way to write “clean” code that allows you to display what you want, but leave the underlying data in a form that’s more useful and/or efficient. Here is the code for the transformer class:

script FullPathToNameTransformer
	property parent : class "NSValueTransformer"
	
	on transformedValueClass()
		return current application's NSString's |class|()
	end transformedValueClass
	
	on allowsReverseTransformation()
		return 0
	end allowsReverseTransformation
	
	on transformedValue_(theValue)
		if theValue is missing value then return missing value
		return theValue's lastPathComponent()
	end transformedValue_
	
end script

The docs say that the first 2 methods in there are necessary, but I’ve found through logging (and commenting them out) that they never seem to be called. I’m not sure what’s going on with that, so I just leave them in.

I also added a double click method for the outline view to show how to get the value of the represented objects, in this case, the full path of the item you double click on.

Ric

Just a nitpicking note:

A NSValueTransformer definition in applicationWillFinishLaunching which is used with bindings could cause problems. A better location is awakeFromNib or even the class method initialize

Stefan,

Thanks for the heads up, I haven’t seen a problem yet, but I see how that could happen, so I added this to my code:

property theTransformer : missing value
	
	on awakeFromNib()
		if theTransformer is missing value then
			set theTransformer to current application's FullPathToNameTransformer's alloc()'s init()
			current application's NSValueTransformer's setValueTransformer_forName_(theTransformer, "NameFromPathTransformer")
		end if
	end awakeFromNib

Ric

Hi Ric!

Thank you so much for posting these examples! They really break the process down nicely! Well done!

I was able to get the “simple” version working with no problems, but I’m having trouble with the second example. Can you walk me through how to add and bind the “FullPathToNameTransformer” class to the project? I’m getting the error:

[AppDelegate applicationWillFinishLaunching:]: unrecognized function filteredArrayUsingPredicate_. (error -10000)

I’d like to get it working so I can go through and see how everything works as the script progresses.

Thanks so much for you help on this!

Robert

I don’t think the error you are seeing has anything to do with the binding. You should try putting a “log theArray” line after the first line in the addToNode_atPath_ method to see if you have an array present. The error you are seeing usually arises from the fact that the thing you are addressing the message to, that is theArray, doesn’t exist.

As for the binding, you don’t bind the class but the name of the transformer that was given in line 4 of the applicationWillFinishLaunching method – NameFromPathTransformer. That should go in the Value Transformer field below the Model Key Path field in the binding for your column.

Ric

Sorry, missed the bit about adding the class file – you go to the File menu in Xcode and choose New File. Choose an applescript class, and name it FullPathToNameTransformer, then just paste in the code

Ric

Thank you so much!! I got it to populate the outline view! You were right about the path. I didn’t see the “FileParser:” directory in the “thePath” variable. Since I didn’t have that folder on my desktop it was failing.

Thanks for explaining about adding the AS class. I was confused because as far as I can tell, Xcode 4.2 doesn’t have an AS class file option, or I’m missing it. So, I saved a FullPathToNameTransformer.applescript file outta Applescript Editor and manually added it to the project. I was able to add it with Xcode 3.2.6 the way you suggested and it worked fine.

Thanks again!!! This will really help me understand how Outline View works. Cheers!

I am having a confounding problem.

After successfully following your guidelines above, I have proceeded to incorporate hierarchical outline views into an application that I am building.

The purpose of the App is to present users with a standard taxonomy of metadata to select from and then applies the validated metadata to documents within a desktop DAM program.

I am creating the hierarchical lists of Metadata Taxonomy as .plist files containing arrays:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
	<dict>
		<key>Asset_Type</key>
		<string>Marketing Publication</string>
		<key>children</key>
		<array>
			<dict>
				<key>Asset_Sub_Type</key>
				<string>Press Release</string>
			</dict>
			<dict>
				<key>Asset_Sub_Type</key>
				<string>Annual Report</string>
			</dict>
		</array>
	</dict>
</array>
</plist>

I am loading the plist into a property Variable named metaData01.

Here is the bare-bones AppDelegate script:


script LCCAppDelegate
	property parent : class "NSObject"
	property metaData01 : {}
	property Asset_TypeOutlineView : missing value
	property AssetTreeController : missing value
    
	
	on applicationWillFinishLaunching_(aNotification)
		set cFile to "/Development/MetaTagger/MetaTagger/assetTypePlistTest.plist"
		loadPlist(cFile)
		Asset_TypeOutlineView's setDoubleAction_("selectAssetType:")
		Asset_TypeOutlineView's setTarget_(me)
	end applicationWillFinishLaunching_
	
	on applicationShouldTerminate_(sender)
		-- Insert code here to do any housekeeping before your application quits 
		return current application's NSTerminateNow
	end applicationShouldTerminate_
    
    on loadPlist(aFile)
        set my metaData01 to current application's NSMutableArray's alloc()'s initWithContentsOfFile_(aFile)
    end loadPlist
    
    on selectAssetType_(sender)
       log sender's itemAtRow_(sender's selectedRow())'s representedObject()
    end selectAssetType_

end script

In Interface Builder, I have one Window and a Tree Controller.

The tree controller is bound to “App Delegate” with a Model Key Path of “metaData01”. The Key Paths: Children field is set to “children”.

The window contains a single Outline View. Within the Scroll View, the Table View has been set to contain a single column whose Value is bound to Tree Controller with Controller Key of “arrangedObjects” and Model Key Path of “representedObject”.

Every thing hooks up great, compiles and runs without error. When I double click on one of the top few lines of the Outline View, the log dutifully returns an array containing the proper clicked value.

HOWEVER…

There is no text visible whatsoever in the Outline View.

I see disclosure triangles for the sub-arrays, and clicking on rows properly highlights them, but the string values are not displayed, now matter what I try.

I must be missing something very obvious.

Any help would be greatly appreciated.

Sincerely,

  • Desperate to Display Data

You have a couple of problems. Your root object and its children must have a common key for a value you want to display – you’re using different keys, which won’t work. When you standardize the key, you then need to use it as the Model Key Path in the binding for the outline view’s column.

Hi there! I have been following this (awesome) guide of yours to create a sample project to understand how to add items to an NSOutlineView. The code compiles, and I have set the bindings as instructed, but when I run the app, I get nothing displayed. I’ll paste some screenshots to see if you can please help me figure out where I messed up:

http://note.io/1hrrHZC

http://note.io/1hrrPbH

http://note.io/1hrs06K

Thanks in advance!

Nevermind, just figured it out!

I moved the code to awakeFromNib from applicationWillFinishLaunching_ and it worked just fine - woot! :):slight_smile: