I'm currently trying to use a new view-based NSOutlineView in my Cocoa app. As I'm not using bindings, so I implemented all required delegate and datasource methods in my controller.
In interface builder I've added a NSOutlineView with a highlighting set to SourceList and Content Mode set to View Based. Thus, there were two default table cell views provided (one Header cell with HeaderCell set as identifier and one data cell with DataCell set as identifier)
This is what it looks like in interface builder, header cell views correctly show a grey-blue textField while data cell views have a image view and a textField with correct color and font settings
To provide the views, I use the following code, to return a DataCell-view or a HeaderCell-view and set the textField of the cell accordingly, based on the corresponding identifier set in interface builder.
- (NSView *)outlineView:(NSOutlineView *)outlineView
viewForTableColumn:(NSTableColumn *)tableColumn
item:(id)item {
NSTableCellView *result = nil;
if ([item isKindOfClass:[NSMutableDictionary class]]) {
result = [outlineView makeViewWithIdentifier:#"HeaderCell" owner:self];
id parentObject = [outlineView parentForItem:item] ? [outlineView parentForItem:item] : groupedRoster;
[[result textField] setStringValue:[[parentObject allKeys] objectAtIndex:0]];
} else {
result = [outlineView makeViewWithIdentifier:#"DataCell" owner:self];
[item nickname] ? [[result textField] setStringValue:[item nickname]] : [[result textField] setStringValue:[[item jid] bare]];
}
return result;
}
Running everything it looks like the following.
Could anybody provide me with hints, to why the header cell is neither bold, nor correctly colored, when selected?
You need to implement the -outlineView:isGroupItem: delegate method and return YES for your header rows. That will standardize the font and replace the disclosure triangle on the left with a Show/Hide button on the right. You will still need to manually uppercase your string to get the full effect.
I'm not sure if the group row delegate method above makes the selection style look okay or not. However, you normally don't want the header rows to be selectable at all in source lists, which you by returning NO for header items from the -outlineView:shouldSelectItem: delegate method.
I have created a little sample project which includes a source list and also uses the -outlineView:isGroupItem: method as #boaz-stuller has suggested.
Display a list of items
Edit the items in a master-detail fashion
Remove and add items
Usage of bindings
Check out besi/mac-quickies on github.
Most of the stuff is either done in IB or can be found in the AppDelegate
Related
I've got a view-based NSTableView, using Cocoa Bindings to change the values of some labels and images in the cell. It all works great. However, I want to add a button to the cell. I've got the button working, but its action method only has the button as sender, which means I have no idea of the content of the cell that the button is in. Somehow I need to store some extra data on the button - at the very least the row index that the button is in. I subclassed NSButton and used my subclass in the cell, but Interface Builder doesn't know about the extra property so I can't bind to it. If I wanted to bind it in code, I don't know the name of the object or keypath that would be passed to it.
How can I get this to work?
You can use rowForView in your action method to get the row value
- (IBAction)doSomething:(id)sender
{
NSInteger row = [_myTableView rowForView:sender];
}
You can use the Identity field in Interface Builder to associate a table cell view from the nib with an instance in your code:
Additionally you have to implement - tableView:viewForTableColumn:row: in your table view's delegate. (Don't forget to connect the delegate in IB)
- (NSView*)tableView:(NSTableView*)tableView viewForTableColumn:
(NSTableColumn*)tableColumn row:(NSInteger)row
{
SSWButtonTableCellView *result = [tableView makeViewWithIdentifier:#"ButtonView" owner:self];
result.button.title = [self.names objectAtIndex:row][#"name"];
result.representedObject = [self.names objectAtIndex:row];
return result;
}
I added representedObject property in my NSTableCellView subclass, which I set in the above table view delegate method.
Your custom table cell view can later use that object in it's action. e.g.:
- (IBAction)doSomething:(id)sender
{
NSLog(#"Represented Object:%#", self.representedObject);
}
I'm using an NSOutlineView with source list style, and using the view based (rather than cell based) outline view.
I would like to be able to make some rows bold. However, my attempts to change the font (manually in IB, through code in viewForTableColumn:…, or through the Font Bold binding) have so far been ignored.
From this message, it appears that this is because the source list style for NSOutlineView takes over managing the text field's appearance:
I'm guessing that you've hooked up your text field to the textField outlet of the NSTableCellView? If so, I think you might be running into NSTableView's automatic management of appearance for source lists.
Try disconnecting the text field from the textField outlet and see if your custom font sticks.
If I disconnect the textField outlet, the appearance does come under my control, and my emboldening works.
However, now I can't get it to look like the automatic one. By which I mean, when NSOutlineView was managing the text field's appearance, the font was bold and gained a drop shadow when any item was selected, but when I'm managing it manually this is not the case.
Can anyone answer either of these questions:
How can I get the Font Bold binding to work when NSOutlineView is managing the appearance of my text field
If I don't have NSOutlineView manage the appearance of my text field, how can I make it look and behave like it would if I did have it manage it?
I think I found the solution:
NSTableCellView manages the appearance of it's textField outlet by setting the backgroundStyle property on cells of contained controls. Setting this to NSBackgroundStyleDark triggers a special path in NSTextFieldCell which essentially sets an attributedStringValue, changing the text color and adding an shadow via NSShadowAttributeName.
What you could do is two things:
Set the backgroundStyle on your own in a custom row or cell view subclass.
Use a custom NSTextFieldCell in the cell's text field and change the behavior/drawing.
We did the latter since we needed a different look for a themed (differently colored) table view. The most convenient (albeit surely not most efficient) location we found for this was to override - drawInteriorWithFrame:inView: and modify the cell's attributed string before calling super, restoring the original afterwards:
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
NSAttributedString *originalString = self.attributedStringValue;
// Customize string as you like
if (/* whatever */)
[self setAttributedStringValue: /* some string */];
// Regular drawing
[super drawInteriorWithFrame:cellFrame inView:controlView];
// Reset string
if (self.attributedStringValue != originalString)
self.attributedStringValue = originalString;
}
In the hope this may help others in similar situations.
Not sure if I have missed anything in your question but changing the font using the following works for me. ReminderTableCellView is just a subclass of NSTableCellView with an additional dateField added.
- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item {
//LOG(#"viewForTableColumn called");
// For the groups, we just return a regular text view.
if ([_topLevelItems containsObject:item]) {
//LOG(#" top level");
NSTableCellView *result = [outlineView makeViewWithIdentifier:#"HeaderCell" owner:self];
// Uppercase the string value, but don't set anything else. NSOutlineView automatically applies attributes as necessary
NSString *value = [item uppercaseString];
[result.textField setStringValue:value];
//[result.textField setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
return result;
} else {
//LOG(#" menu item");
// The cell is setup in IB. The textField and imageView outlets are properly setup.
// Special attributes are automatically applied by NSTableView/NSOutlineView for the source list
ReminderTableCellView *result = [outlineView makeViewWithIdentifier:#"DataCell" owner:self];
if ([item isKindOfClass:[OSTreeNode class]]) {
[result.textField setFont:[NSFont boldSystemFontOfSize:13]];
result.textField.stringValue = [item displayName];
result.dateField.stringValue = [item nextReminderDateAsString];
}
else
result.textField.stringValue = [item description];
if (_loading)
result.textField.textColor = [NSColor grayColor];
else
result.textField.textColor = [NSColor textColor];
NSImage *image = [NSImage imageNamed:#"ReminderMenuIcon.png"];
[image setSize:NSMakeSize(16,16)];
[result.imageView setImage:image];
//[result.imageView setImage:nil];
return result;
}
}
Resulting view is shown below. Note this is is an NSOutlineView with Source Listing option selected but I can't see why this would'nt work for a normal outlineView.
I am trying to create a Core Data app where the user is organizing a lot of information into sections. I have a main xib that has a popup menu and a non-bordered box. Into that box, a separate xib will be loaded with the view for the section chosen from the popup button.
I decided to make a second window/panel that's a sort of accessory window. The idea is that the main window shows a summary table, while the accessory view makes it easier to input data by taking the current selection in the summary table and displaying it in text fields, graphical date pickers (instead of forcing the user to use the correct format for typing a date into the table), etc. It also holds some optional fields and displays stats, so those don't clog up my main view.
My Document.m for the main xib has:
- (id)init
{
self = [super init];
if (self) {
viewControllers = [[NSMutableArray alloc] init];
accessoryViewControllers = [[NSMutableArray alloc] init];
ManagingViewController *vc;
ManagingViewController *accessoryVC;
vc = [[SummaryViewController alloc] init];
accessoryVC = [[SummaryAccessoryViewController alloc] init];
[vc setManagedObjectContext: [self managedObjectContext]];
[accessoryVC setManagedObjectContext: [self managedObjectContext]];
[viewControllers addObject: vc];
[accessoryViewControllers addObject: accessoryVC];
}
return self;
}
And so on for the other viewControllers/xib files that will be listed in the popup button. Making a selection in the popup returns its sender tag, then calls another method that takes the tag, and loads the objectAtIndex in the vc array into the main window box and accessoryVC array into the accessory window. In the actual SummaryViewController.m I have:
- (id) init {
self = [super initWithNibName: #"SummaryView" bundle:nil];
if (self) {
[self setTitle: #"Summary"];
}
return self;
}
I built all the views, then started binding. A column in the table in the main window might be bound to arrangedObjects.aaa and the accessory view's textfield will be bound to selection.aaa, but its selection won't change when the tableview selection changes. I'm guessing that's because technically they're using two separate NSArrayControllers.
I've seen examples in books where a secondary window had data synched to the main window, and it worked because both windows came from the same xib, so they used the same NSArrayController. My question is, which of these options can I use:
1) Is there a way to make the NSArrayControllers stay in synch across multiple xib files?
2) I could move the custom view in the SummaryAccessoryView.xib into SummaryView.xib so that the one xib contains both the view for the main and accessory windows. Then they would share NSArrayControllers. But then how do I get my popup to put one view in the main window and the other in the accessory window? My current method relies on [super initWithNibName: SummaryView.xib] so I don't see any way to specify which view.
3) I guess I could cave and rebuild the whole thing to a one-window model, scrap the redundant fields and put the extra fields at the bottom part of my main view, but the user won't be able to hide it or move it around and I have that issue again with having a user formatting their dates into a tableview... It might work if I knew how to have a graphical date picker come up when the user clicks a table cell. But I'd prefer to keep the two-window model if possible.
Any ideas on how to do option 1 or 2?
EDIT: I got option 3 working:
You need a few ivars first: a date picker (myDatePicker), your table (myTable), the pop-over that houses the date picker (myPopover), and the NSArrayController (myArray). Also in my example, the date column is the first column (column 0) and I've named it in IB as "date". If you have multiple dates (like start/end dates or two tables), you can add in an NSString ("tableAndColumn") that uses #define to set flags to identify which date you need, and turn your if statement into an if-else with multiple cases.
- (BOOL) tableView:(NSTableView *)tableView
shouldEditTableColumn:(NSTableColumn *)tableColumn
row:(NSInteger)row {
if (tableColumn == [myTable tableColumnWithIdentifier: #"date"]) {
//tableAndColumn = myStartDate;
[myDatePicker setDateValue: [myArray valueForKeyPath: #"selection.date"]]; //this will set your date picker to the value already in the table column
NSRect rect = [myTable frameOfCellAtColumn: 0 row: [myTable selectedRow]];
[myPopover showRelativeToRect: rect ofView: myTable preferredEdge:NSMaxYEdge];
return NO;
// } else if (tableColumn == [myTable tableColumnWithIdentifier: #"endDate"]) {
// ...
} else {
return YES;
}
}
- (void) popoverWillClose:(NSNotification *)notification {
// if ([tableAndColumn isEqualToString: MyStartDate]) {
[myArray setValue: [myDatePicker dateValue] forKeyPath: #"selection.date"];
// } else if ([tableAndColumn isEqualToString: MyEndDate]) {
// ...
// }
}
You can bind to your array controllers across NIB files by using properties of your NIB file's owner that are key-value coding and key-value observing compliant. E.g. if one of your NIB files has your NSViewController subclass as the file's owner, you can bind controls to the file's owner using key paths that start with representedObject.
In your example, you could store your view controllers (which you initialized in -[Document.m init]) in dedicated properties, and set the NSViewController's representedObject to the document instance. Then, in your NIB file, you could bind your controls to the file's owner using a key path that starts with representedObject.myViewControllerProperty.myArrayControllerProperty etc.
In my own app, I initiate a custom window controller in -[Document makeWindowControllers] using -initWithWindowNibName and store it in a mainWC property. This main window controller creates subordinate view controllers (similar to how you've done it) and sets their representedObject property to itself ([vc setRepresentedObject:self]). All bindings in other NIB files are then routed thru this main window controller via bindings to file's owner using key paths that start with representedObject.
In a similar fashion, my MainMenu.xib file connects e.g. the "Enabled" property of some menu commands to appropriate array controller properties by binding to the Application object using key paths that start with mainWindow.windowController.document.mainWC.
I'm trying to customize the disclosure arrow appearance in my view-based NSOutlineView. I saw that it's recommended to use
- (void)outlineView:(NSOutlineView *)outlineView willDisplayOutlineCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
delegate method to achieve it. The problem is that this method is not called for some reason. I have 2 custom cell views - one for item and second for header item. May be this method is not called for view-based outline views? May be something became broken in Lion?
Please shed some light.
Solution 1:
Subclass NSOutlineView and override makeViewWithIdentifier:owner:
- (id)makeViewWithIdentifier:(NSString *)identifier owner:(id)owner {
id view = [super makeViewWithIdentifier:identifier owner:owner];
if ([identifier isEqualToString:NSOutlineViewDisclosureButtonKey]) {
// Do your customization
}
return view;
}
For Source Lists use NSOutlineViewShowHideButtonKey.
Solution 2:
Interface Builder
The button is added to the column and the identifier set to NSOutlineViewDisclosureButtonKey.
Official documentation from NSOutlineView.h
/* The following NSOutlineView*Keys are used by the View Based NSOutlineView to create the "disclosure button" used to collapse and expand items. The NSOutlineView creates these buttons by calling [self makeViewWithIdentifier:owner:] passing in the key as the identifier and the delegate as the owner. Custom NSButtons (or subclasses thereof) can be provided for NSOutlineView to use in the following two ways:
1. makeViewWithIdentifier:owner: can be overridden, and if the identifier is (for instance) NSOutlineViewDisclosureButtonKey, a custom NSButton can be configured and returned. Be sure to set the button.identifier to be NSOutlineViewDisclosureButtonKey.
2. At design time, a button can be added to the outlineview which has this identifier, and it will be unarchived and used as needed.
When a custom button is used, it is important to properly set up the target/action to do something (probably expand or collapse the rowForView: that the sender is located in). Or, one can call super to get the default button, and copy its target/action to get the normal default behavior.
NOTE: These keys are backwards compatible to 10.7, however, the symbol is not exported prior to 10.9 and the regular string value must be used (i.e.: #"NSOutlineViewDisclosureButtonKey").
*/
APPKIT_EXTERN NSString *const NSOutlineViewDisclosureButtonKey NS_AVAILABLE_MAC(10_9); // The normal triangle disclosure button
APPKIT_EXTERN NSString *const NSOutlineViewShowHideButtonKey NS_AVAILABLE_MAC(10_9); // The show/hide button used in "Source Lists"
This answer is written with OS X 10.7 in mind, for newer versions of OS X/macOS, refer to WetFish's answer
That method does not get called because it is only relevant for cell based outline views.
In a view based outline view, the disclosure triangle is a regular button in the row view of expandable rows. I don't know where it gets added, but it does, and NSView's didAddSubview: method handles exactly that situation of a view being added somewhere else.
Hence, subclass NSTableRowView, and override didAddSubview:, like this:
-(void)didAddSubview:(NSView *)subview
{
// As noted in the comments, don't forget to call super:
[super didAddSubview:subview];
if ( [subview isKindOfClass:[NSButton class]] ) {
// This is (presumably) the button holding the
// outline triangle button.
// We set our own images here.
[(NSButton *)subview setImage:[NSImage imageNamed:#"disclosure-closed"]];
[(NSButton *)subview setAlternateImage:[NSImage imageNamed:#"disclosure-open"]];
}
}
Of course, your outline view's delegate will have to implement outlineView:rowViewForItem: to return the new row view.
Despite the name, frameOfOutlineCellAtRow: of NSOutlineView still gets called for view based outline views, so for the positioning of your triangle, you might want to subclass the outline view and override that method, too.
For Swift 4.2 macOS 10.14, #WetFish's answer can be implemented as follows:
class SidebarView: NSOutlineView {
override func makeView(withIdentifier identifier: NSUserInterfaceItemIdentifier, owner: Any?) -> NSView? {
let view = super.makeView(withIdentifier: identifier, owner: owner)
if identifier == NSOutlineView.disclosureButtonIdentifier {
if let btnView = view as? NSButton {
btnView.image = NSImage(named: "RightArrow")
btnView.alternateImage = NSImage(named: "DownArrow")
// can set properties of the image like the size
btnView.image?.size = NSSize(width: 15.0, height: 15.0)
btnView.alternateImage?.size = NSSize(width: 15.0, height: 15.0)
}
}
return view
}
}
Looks quite nice!
Swift2 version of #Monolo's answer:
override func didAddSubview(subview: NSView) {
super.didAddSubview(subview)
if let sv = subview as? NSButton {
sv.image = NSImage(named:"icnArwRight")
sv.alternateImage = NSImage(named:"icnArwDown")
}
}
What I basically trying to achieve is to change the formatting of a cell in a table to blank it out if the cell is not enabled.
To define whether a cell in a table column is enabled I've bound using the enabled binding and this is working fine, enabling the cells in some rows of the table and not in others.
I have defined a sub-class of NSTextFieldCell that I am using to change the format of the cell. This also appears to be working fine, but I am having trouble triggering the change depending on whether the cell in the table is enabled. Originally I tried to activate the switch by calling [self isEnabled] on the cell. This worked but the switch never activated, which I assume is because the original binding defining whether a cell is enabled was to the table column rather than to the cell.
Does anyone have a method to easily achieve this. The only method I have noticed would be to sub-class the NSTableColumn and write a custom version of dataCellForRow: but this somehow doesn't seem like the best way to solve this problem.
I use the table view's delegate method to set the text color before displaying a cell. You could set the cell color to the background color to make it invisible when the cell is disabled.
- (void)tableView:(NSTableView *)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex
{
NSColor* theColor = [NSColor blackColor];
BOOL enable = YES;
if(![self tableView:aTableView shouldSelectRow:rowIndex])
{
theColor = [aCell backgroundColor];
enable = NO;
}
[aCell setTextColor:theColor];
[aCell setEnabled:enable];
}
I tested this code briefly and it worked. I am not using bindings, but that shouldn't matter for this as long as the table's delegate is set up properly.
Thankyou for your answer Mark. It didn't seem to work for me but it did point me in the right direction. I can't quite do everything I want because filling a cell with a background colour is a little harsh with the square edges but it works fairly robustly so I'll use it for now. The actually code I used is:
- (void)tableView:(NSTableView *)aTableView
willDisplayCell:(id)aCell
forTableColumn:(NSTableColumn *)aTableColumn
row:(int)rowIndex
{
NSColor *cellColor = [NSColor lightGrayColor];
if([aCell isEnabled])
{
cellColor = [NSColor whiteColor];
}
if([aCell isKindOfClass:[NSTextFieldCell class]])
{
[aCell setDrawsBackground:YES];
}
[aCell setBackgroundColor:cellColor];
}