ASObjC Source List with Static Values

Hey, it’s Ted. Can you give some idea of the kind of styling you want? It’s easier to work with something specific.

Sure. So, this is the code that I pulled from the link I posted previously. I wired it into an interface following the instructions on that page and it creates a table with my values formatted either as a header or regular.

script AppDelegate
    property parent : class "NSObject"

    
    -- IBOutlets
    property theWindow : missing value
    property arrayController : missing value
    property detailView : missing value
    property refTable : missing value
    
    
    on applicationWillFinishLaunching_(aNotification)
        -- Insert code here to initialize your application before any files are opened
        (* set up list of headers and lines for the side bar *)
        set sidebarList to {{title:"Header 1", isHeader:true}, {title:"Line 1", isHeader:false}, {title:"Header 2", isHeader:true}, {title:"Line 2", isHeader:false}, {title:"Line 3", isHeader:false}, {title:"Header 3", isHeader:true}, {title:"Line 4", isHeader:false}}
        arrayController's addObjects:sidebarList
        
    end applicationWillFinishLaunching_
    
    on applicationShouldTerminate_(sender)
        -- Insert code here to do any housekeeping before your application quits
        return current application's NSTerminateNow
    end applicationShouldTerminate_
    
    (* table view delegate methods *)
 
    on tableView:tableView isGroupRow:row
    
        -- header rows are group rows
        set rowData to arrayController's arrangedObjects's objectAtIndex:row
        return rowData's isHeader
    end

   
    on tableView:tableView shouldSelectRow:row
        -- don't want to select header rows
        set rowData to arrayController's arrangedObjects's objectAtIndex:row
        return not (rowData's isHeader)
    end
   
    on tableView:tableView viewForTableColumn:column row:row
        -- header rows get a special look
        set aView to tableView's makeViewWithIdentifier:"tableItem" owner:me
        return aView
    end
    
    on tableViewSelectionDidChange:aNotification
        (*
         This is method gets notified right after a selection is made. This is one of
         the places where you can change the detail view to show a the correct view for
         selected sidebar item. For demonstration purposes I'm just swapping out
         TextField views with the name of the sidebar item. Not to sophisticated,
         but it get the point across
         *)
        set tableView to aNotification's object
        set selectedRowIdx to (tableView's selectedRow) as integer
        log "Picked Row " & selectedRowIdx
        set rowData to arrayController's arrangedObjects's objectAtIndex:selectedRowIdx
        set newLabel to current application's NSTextField's labelWithString:(rowData's title )
        set newLabel's translatesAutoresizingMaskIntoConstraints to false
        set detailSubviews to (detailView's subviews) as list
        if detailSubviews is not {} then
            set oldLabel to item 1 of detailSubviews
            detailView's replaceSubview:oldLabel |with|:newLabel
            else
            detailView's addSubview:newLabel
        end
        
        set constraintX to newLabel's centerXAnchor's constraintEqualToAnchor:(detailView's centerXAnchor)
        set constraintY to newLabel's centerYAnchor's constraintEqualToAnchor:(detailView's centerYAnchor)
        constraintX's setActive:true
        constraintY's setActive:true
    end
    
end script

The regular entries are largely ok, looking like the normal font/size/alignment for table rows. But the heads are the same font/size as regular (which I might want to change) but, worse, they have extra height, are aligned to the top of that and have a gray background and are bordered by horizontal lines. Not only do I not now how to change that formatting, but I’m not completely clear where to put the code in the script. I am used to putting table data in place via bindings instead of addObjects and still have a major mental block when it comes to ASObjC and translating an Apple ObjC documentation.

I also see a potential hurdle in programmatically selecting a specific row… and, by default, this code is selecting every row when it populates the table. So that is another issue I could use some pointers on.

I hope that all makes sense.

Any advice you can offer would be most appreciated.

You don’t need to post the code for me; I’m the one who wrote it over at Stackoverflow, so I have a copy of it.

Simplest things first. To keep the table from selecting every row on insert, un-click ‘Select inserted objects’ in the Attributes Inspector of the Array Controller object. And don’t worry about selecting a row programmatically. whenever the selection changes, regardless of how or why it changes (with a few minor exceptions), the tableview will call its delegate and function properly. On a certain level you have to trust that things will work; avoid worrying until you see an actual problem.

With respect to visual aesthetics… First, in the table view’s Attributes Inspector, you might want to change the highlight option to SourceList. It’s a smoother look, but it’s more difficult to adjust programmatically. (Sorry, I’m not sure how to post images here, or I’d show you)

There are two kinds of view you see in a table view: Table Cell Views (NSTableCellView) and Table Row Views (NSTableRowView). Cell views are the normal views used to display table cells in columns; row views are views used to display groups rows (like the headers used in this sidebar). You have to at least one cell view defined in the nib, but the table view will define a default row view for you (which is what you’re seeing currently). You can, of course, create as many cell and row views as you like and call them programmatically (by identifier) for different contexts.

These delegate methods allow you to assign (or even programmatically create) row and cell views for specific data items:

on tableView:tableView viewForTableColumn:tableColumn row:row
    --
end

on tableView:tableView rowViewForRow:row
    --
end

… while these delegate methods allow you to catch a cell or row before display and modify some of its visual aspects:

on tableView:tableView willDisplayCell:cell forTableColumn:tableColumn row:row
    --
end

on tableView:tableView didAddRowView:rowView forRow:row
    --
end

You might also find this method useful, which lets you set row heights individually:

on tableView:tableView heightOfRow:row
    --
end

As far as specific implementations go, I’ll throw out two options:

Option 1

This is easiest, and only involves going to the Table View’s Attributes inspector in the xib and changing the Highlight option to Source List. Source lists have a smoother look which you might like out of the box, but it is hard to change header items (I think it’s possible, but I’d have to play with it a bit). For instance, if you add this method:

on tableView:tableView didAddRowView:rowView forRow:row
	set rowCell to rowView's subviews's objectAtIndex:0
	set rowTextField to rowCell's textField
	rowTextField's setTextColor:(current application's NSColor's greenColor)
end

it will change all of the regular lines to green, but not the headers.

Option 2

This involves forgetting about using grouping rows (which aren’t really necessary anyway since this is a simple, flat source list). Keep the Table View’s Highlight option at Regular, delete the entire ‘tableView:isGroupRow:’ method, and then pull a second Table Cell View out of the object library and add it to the table column. Give that one the identifier (say) ‘headerItem’, and rewrite the ‘tableView:viewForTableColumn:row:’ method like so:

on tableView:tableView viewForTableColumn:column row:row
	-- header rows get a special look
	set rowData to arrayController's arrangedObjects's objectAtIndex:row

	if rowData's isHeader as boolean is false then
		set aView to tableView's makeViewWithIdentifier:"tableItem" owner:me
	else
		set aView to tableView's makeViewWithIdentifier:"headerItem" owner:me
	end
	return aView
end

Then style the two Table Cell Views in IB to get the look you want for the headers and rows. This method will now call a different view for headers and regular items.

Ted,

Thank you much for the detailed response and for that original post which was hugely useful.

Just changing the Table’s highlight to Source List made a huge difference and a leap towards what I want. Didn’t know that was an option.

Formatting

So now I am less worried about font/size/color type applications and more wondering if I can get those headers to hug the bottom of their row instead of the top… or possibly make the row height the same. I’m not sure if I missed a step, but when I put the tableView:tableView heightOfRow:row in there with only a logging comment to see when it is hit I got a crash.

Another tweak that would be nice is to be able to affect the indent of headers and body rows. Right now they are very close to the left side of the table.

I’ll keep exploring that and see if I can’t figure it out but might need guidance on what can be done to make it look one notch nicer.

Selection

The initial selection issue is resolved too with that setting. However, now nothing is selected at the start and I do need to be able to programmatically make a selection… at least at the beginning. I tried selectRow and got weird results.

I got things to indent by changing the Table View Cell’s X setting in the View area of the Size Inspector… FYI

I also got it to programmatically select with this:


 set indexSet to current application's NSMutableIndexSet's alloc()'s init()
indexSet's addIndex_(1)
tell refTable to selectRowIndexes_byExtendingSelection_(indexSet, false)

Not sure if that’s the best way…

Now, if I could just get the headers to align bottom.

Is there a similar handler for header rows like the one you posted:

tableView:tableView didAddRowView:rowView forRow:row

To get the headers to align bottom, try combining Options 1 and 2. You’ll use the Source List setting, and keep the ‘tableView:isGroupRow’ method to make the headers group rows as in Option 1 (that gives you the Source List look). But then you’ll add a second Table Cell View to the table (you can just duplicate the first one), give it the identifier ‘headerItem’, and use the ‘tableView:viewForTableColumn:row:’ method outlined in Option 2.

Now go to that second Table Cell View in the xib, click on its Table View Cell, go to the size inspector, and look at the autoresizing box near the bottom. You’ll see that it’s attached at the left, right, and top. Click on the top attachment to turn that off, then click on the bottom to turn on the attachment there. That’s good to go.

If I were going to try to indent the table cell, I would probably either change the tableView’s intercellSpacing property or set actual layout constraints on the TextFiledView in the xib. But whatever works, works…

Best way to set the selection is to go through the array controller. You can use this command to (for instance) set the selection to Line 1;

arrayController's setSelectionIndex:1

I’m not sure what will happen if you try to select one of the headers.

The equivalent of ‘didAddRow:’ is ‘willDisplayCell:’, as I noted above.

THAT WORKS!!! THANK YOU!!!

I have a beautiful looking source list, with non-selectable groups, a selection of an item below it triggers a subroutine… Will need to clean up my code a little and then try transplanting this test into my app.

FYI one thing that is weird is that the first header is stuck in place and when you scroll the item below overlaps it… This won’t affect me because the list is static and will always fit without scrolling, but if there is a fix for that it might be nice.

Also a little weird… everything works fine but I notice it is now generating two errors right away:

Failed to set (title) user defined inspected property on (NSArrayController): [<NSArrayController 0x600003d00c30> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key title.
Failed to set (isHeader) user defined inspected property on (NSArrayController): [<NSArrayController 0x600003d00c30> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key isHeader.

Per the first point: xib->Table View->Attributes inspector: is the ‘Float group rows’ checkbox clicked off? sounds like it’s on…

For the error, I’m not seeing that on my end. It’s odd, in fact, that the code is even trying to set the title anywhere. Can you narrow down where that error is occurring? Usually Xcode gives a line number or a method name.

First point was the problem. Thanks.

For the errors, I don’t see a line number and I can confirm that it is happening before anything in applicationWillFinishLaunching_ runs. But I found that I had those two keys entered in two places in the table settings…user defined runtime areas… and when I remove them the error goes away and so fare everything still works. So that might now be a non-issue.

I wonder if it is possible to add a second column to the regular title entries?

That’s easy enough: go to the table view’s Attributes inspector and change the columns from 1 to 2. Then you’ll have to set up and bind a new table cell view for the new column.

Thanks for all the help @Ted Wrigley, glad my question and your answer are contributing to others,
i’m still trying to figure out how to use a .xib file for each selected sidebar item.
Could you help me with this?

Hah! Sorry, I thought that Mark was you: he was following the exact same line of thought, and even linked back to the StackOverflow discussion we were having. Too funny… :smiley:

I’ll get back to you on this tomorrow afternoon. I know how to implement this in theory, but I’ve never actually done it, so some puddling around is in order.

Mark must be trying to do the same as me, or we had the same idea … really really funny :lol: ok wait for your reply Ted, thanks for the help always.

Ok, so here’s how you use a nib file (at least one way of doing it, but it’s the one that makes most sense to me…).

First, change the model. In the AppDelegate file’s ‘applicationWillFinishLaunching:’ handler, change the list of dictionaries to add a new value “nibName” that holds the name of the nib we want to invoke. It should look something like this:

set sidebarList to {{title:"Header 1", isHeader:true, nibName:""}, {title:"Line 1", isHeader:false, nibName:"Line1"}, {title:"Header 2", isHeader:true, nibName:""}, {title:"Line 2", isHeader:false, nibName:"Line2"}, {title:"Line 3", isHeader:false, nibName:"Line3"}, {title:"Header 3", isHeader:true, nibName:""}, {title:"Line 4", isHeader:false, nibName:"Line4"}}

Next, create a view controller. Add a new blank file to the project: Choose New File, scroll down to the ‘Other’ section, choose ‘Empty’, and then click the ‘Next’ Button. Name the file “MyViewController.applescript” (or whatever makes sense to you, with a .applescript extension). Open this new file and edit in the following:

script MyViewController
	property parent: class "NSViewController"

	on initWithNibName:nibName bundle:bundle
		continue initWithNibName:nibName bundle:bundle
	end

	on loadView()
		continue loadView()
	end
end script

The parent class has to be NSViewController (it defualts, I think, to NSObject), the name of the script is the name of the class you’ll invoke later, and you’ll need to override at least these two methods explicitly. continue is the AppleScript equivalent of ‘super’, so all you’re really doing here is telling the subclass to call the objective-C class implementations. You’ll probably need to do on override like this for any method you call explicitly from AppleScript, should you decide to expand.

Next, create your xib files. Choose New File, scroll down to the ‘User Interface’ section, choose ‘View’, and then click the ‘Next’ Button. Use the names in your data model (e.g., if a ‘nibName’ in the model is “Line2” you should have a name corresponding file Line2.xib; I believe Xcode will automatically add the xib extension). Construct the contents of the view as you see fit, then do this:

  1. In the xib view, click on File’s Owner
  2. Click on the Identity inspector, and make sure the file’s owner class is “MyViewController”, or whatever you named your view controller class. It has to correspond to the first line of the file: “script MyViewController”
  3. Click on the Connections inspector, and drag to connect the ‘view’ outlet of the File’s Owner to the top-level of the view you’ve created.

Of course, here you’ll have to do whatever bindings and connection you want to make for this particular view. Anything you set as a property in the view controller can be accessed by the view through the parent object, and that’s how you’ll pass data into the view. But I’m sure you can figure that out.

Now you’re ready to go: back in the AppDelegate file, change the ‘tableViewSelectionDidChange’ like so (note again that I’m using the classname ‘MyViewController’, but you may end up changing that):

on tableViewSelectionDidChange:aNotification
	set tableView to aNotification's object
	set selectedRowIdx to (tableView's selectedRow) as integer
	set rowData to arrayController's arrangedObjects's objectAtIndex:selectedRowIdx
	set theNibName to (rowData's nibName) as text
	if theNibName is not "" then
		set viewController to current application's class "MyViewController"'s alloc's initWithNibName:(rowData's nibName) bundle:(current application's NSBundle's mainBundle)
		viewController's loadView()
		set theView to viewController's view
		set theView's translatesAutoresizingMaskIntoConstraints to false
		set detailSubviews to (detailView's subviews) as list
		if detailSubviews is not {} then
			set oldLabel to item 1 of detailSubviews
			detailView's replaceSubview:oldLabel |with|:theView
		else
			detailView's addSubview:theView
		end

		(* set view constraints *)
		set constraintLeading to theView's leadingAnchor's constraintEqualToAnchor:(detailView's leadingAnchor)
		set constraintTop to theView's topAnchor's constraintEqualToAnchor:(detailView's topAnchor)
		set constraintBottom to theView's bottomAnchor's constraintEqualToAnchor:(detailView's bottomAnchor)
		set constraintTrailing to theView's trailingAnchor's constraintEqualToAnchor:(detailView's trailingAnchor)
		constraintLeading's setActive:true
		constraintTop's setActive:true
		constraintBottom's setActive:true
		constraintTrailing's setActive:true
    end if
end

All this does is create a view for the appropriate xib, load its view, and then add and constrain that view to the detailView, as before.

Just so it’s said, best practice would really be to eliminate the detailView object and use an NSLayoutGuide instead, but this approach strikes me as more accessible.

I suspect you’re doing it the hard way. Create the Nib in Interface Builder as normal, and set its owner to the owner of the table’s nib. Make any connections you need there. Somewhere early in your code run this:

set cell1Nib to current application's NSNib's initWithNibNamed:"<name of your nib>" bundle:(missing value)
myTableView's registerNib:call1Nib forIdentifier:"<some identifier>"

Then you can use that identifier in your tableView:viewForTableColumn:row: handler, and make any changes you need at that point.

Well, except these views are for the detail view, not part of the table view itself. It still might be a better approach; I think I saw you use this approach in some other thread at one point or another, and it crossed my mind to use it, but I’m just so $%^*#$ used to using view controllers that I balked. :rolleyes: Maybe tomorrow I’ll edit in a PS with your suggestion, just to see how it feels.

Yours would certainly be a better approach for adding custom table cell views, but neither Mark nor TheIaMon seems to want to go beyond the default image/textfield view.