How to configure content for NSPopUpButton - cocoa

I have a NSPopUpButton configured with bindings and coredata. Everything is working perfectly, however I would like to add a item that implements an action to "edit the list", like
Item 1
Item 2
Item 3
Item 4
------
Edit List..
Is this Possible to do with Bindings?
I think that the answer is NO, at least not completely. I thought I would provide the content to the button programatically and maintain bindings for the Selected Value , so this is what I came up with
- (void)updateSectorPopupItems
{
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:#"Sector"];
NSSortDescriptor *sortPosition = [[NSSortDescriptor alloc] initWithKey:#"position" ascending:YES];
[request setSortDescriptors:#[sortPosition]];
NSError *anyError = nil;
NSArray *fetchObjects = [_gdcManagedObjectContext executeFetchRequest:request
error:&anyError];
if (fetchObjects == nil) {
DLog(#"Error:%#", [anyError localizedDescription]);
}
NSMutableArray *sectorNames = [NSMutableArray array];
for (NSManagedObject *sector in fetchObjects) {
[sectorNames addObject:[sector valueForKey:#"sectorCatagory"]];
}
[_sectorPopUpBotton addItemsWithTitles:sectorNames];
NSInteger items = [[_sectorPopUpBotton menu] numberOfItems];
if (![[_sectorPopUpBotton menu] itemWithTag:1] ) {
NSMenuItem *editList = [[NSMenuItem alloc] initWithTitle:#"Edit List..." action:#selector(showSectorWindow:) keyEquivalent:#""];
[editList setTarget:self];
[editList setTag:1];
[[_sectorPopUpBotton menu] insertItem:editList atIndex:items];
}
A couple of problems I'm having with this
1) When adding the Menu Item using
[_sectorPopUpBotton menu] insertItem:editList atIndex:items];
no matter what value is entered in atIndex, the item always appears at the top of the Menu list.
2) I just want the "Edit List..." menuitem to initiate the action, how do I prevent this from being selected as a value?

You might as well do that using an NSMenuDelegate method.
Actually in this way you can also keep the bindings for getting the NSPopUpButton content objects (in your case from the NSArrayController bound to the CoreData stack).
1) Set an object as delegate for the NSPopUpButton internal menu, you can do that in the Interface Builder by drilling down the NSPopUpButton to reveal its internal menu. Select it and then set its delegate in the Connections Inspector panel to the object you have designated to this task. As such delegate you might for example provide the same ViewController object which manages the view where the NSPopUpButton exists.
You'll then need to have the object provided as delegate adhere to the NSMenuDelegate informal protocol.
2) Implement the NSMenuDelegate method menuNeedsUpdate: there you'll add the NSmenuItem(s) (and eventually separators) you want to provide in addition to those already fetched by the NSPopButton's bindings.
An example code would be:
#pragma mark NSMenuDelegate
- (void)menuNeedsUpdate:(NSMenu *)menu {
if ([_thePopUpButton menu] == menu && ![[menu itemArray] containsObject:_editMenuItem]) {
[menu addItem:[NSMenuItem separatorItem]];
[menu addItem:_editMenuItem];
}
}
In this example the _editMenuItem is an NSMenuItem property provided by the object implementing this NSMenuDelegate method. Eventually it could be something as this:
_editMenuItem = [[NSMenuItem alloc] initWithTitle:#"Edit…" action:#selector(openEditPopUpMenuVC:) keyEquivalent:#""];
// Eventually also set the target for the action: where the selector is implemented.
_editMenuItem.target = self;
You'll then implement the method openEditPopUpMenuVC: to present to the user the view responsible for editing the content of the popUpButton (in your case the CoreData objects provided via bindings).
The only problem I haven't yet solved with this approach is that when getting back from the view where the edit happens, the NSPopUpButton will have the new item "Edit…" selected, rather than another "valid" one, which is very inconvenient.

Related

Trying to make a tabbed browser

I'm having a problem with cocoa, when I run the app the tabs get added as expected, but all the web views take the same string from the url field.
Basically, if I go to google on one tab, it goes to google on all of them.
Is there any way to make only the web view on the selected tab respond, and not on the others?
Here is the code:
- (IBAction)newTab:(id)sender {
NSTabViewItem *item = [NSTabViewItem new];
[item setView:_webView];
[item setLabel:#"New Tab"];
[_tabView addTabViewItem:item];
}
It looks like you're creating a new tab and then moving your WebView to it. To create a new web view for each tab, you have some options but one is to use NSViewController:
If you create the web view in the same xib as the tab view, move it to a separate xib. Change the class of the owner of that xib to NSViewController.
When adding a new tab in your code, load the xib (assuming it is named WebView.xib):
- (IBAction) newTab: (id) sender
{
NSTabViewItem *item = [[NSTabViewItem alloc] init];
NSViewController *viewController =
[[NSViewController alloc] initWithNibName: #"WebView" bundle: nil];
WebView *webView = [viewController view];
[item setView: webView];
[_tabView addTabViewItem: item];
[_webViewControllers addObject: viewController]; // Store the view controller, remove when the user closes the tab.
}
Here's a tutorial on view controllers: http://comelearncocoawithme.blogspot.fi/2011/07/nsviewcontrollers.html

Synching Core Data with Two Xib Simultaneously

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.

View-based NSOutlineView header cell font issues

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

Preference Pane & NSTableView bindings

I am trying to create a preference pane which will reside within system preferences. All the bindings are done as usual (for a normal windowed application), but when the setter for the binding property is called (data is updated), the table data does not reset. Are preference panes capable of updating table data via bindings? I have also tried to use a table data source unsuccessfully.
To clarify, I have an NSMutableArray property in my prefPane's main class, an object representing the prefPane's main class, and an arrayController in IB which is bound to the table column. in the init method of the prefPane's main class, I set the value of the NSMutableArray, which is properly reflected in the pref pane, however, (just to test if bindings work), i have an NSTimer which resets the value of my NSMutable array when it finishes. A console message tells me that the value is properly reset, however, the changes are not reflected in the pref pane.
So in my current version i use the following code to set the properties to arbitrary values (simplified to try to get bindings to work at all). The property value is then reset by a timer 10 seconds later. Although the property is correctly updated (verified by console log), the pref pane does not reflect the changes in the tableview. Unfortunately, I cannot post screenshots of the bindings. I have an object in IB for the syncFrontEndPref class. I then have an arraycontroller bound to this object w/ a model key path of listArray. Then my table column is bound to the arraycontroller arranged objects. This loads properly with "test", "test1", "test2" in the pref pane (as populated from the init method). However, when repopulated from the timer, the changes are not reflected in the pref pane (although console log confirms listArray has indeed changed.
Here is the code:
#interface syncFrontEndPref : NSPreferencePane
{
NSMutableArray *listArray;
NSNumber *syncInt;
AuthenticateUser *newUser;
NSMutableArray *syncIntervalList;
IBOutlet NSTableView *theTableView;
}
#property (retain) NSMutableArray *listArray;
#property (retain) NSMutableArray *syncIntervalList;
- (void) mainViewDidLoad;
-(IBAction)syncIntervalValueChanged:(id)sender;
-(IBAction)tableViewSelected:(id)sender;
#implementation syncFrontEndPref
#synthesize listArray, syncIntervalList;
-(id) init{
//populate nsarray w/ list data to display
//[self setListArray: [NSMutableArray arrayWithArray:[[[NSDictionary dictionaryWithContentsOfFile:[GetFilePath pathForFile]] objectForKey:#"lists"] allObjects]]];
[self setListArray: [NSMutableArray arrayWithObjects: #"test", #"test1", #"test2", nil]];
//define values for drop-down sync interval selector
[self setSyncIntervalList:[NSMutableArray arrayWithObjects: #"1 minute", #"5 minutes", #"10 minutes", #"30 minutes", #"24 hours", nil]];
return self;
}
//code for the timer and selector method
- (void) mainViewDidLoad{
NSTimer *timer = [[NSTimer new] autorelease];
int syncTime = 10;
timer = [NSTimer scheduledTimerWithTimeInterval: syncTime target:self selector:#selector(targetMethod:) userInfo:nil repeats: YES];
}
-(void)targetMethod:(id)sender{
NSLog(#"running timer...");
[self setListArray: [NSMutableArray arrayWithObjects: #"0", #"1", #"2", nil]];
NSLog(#"%#", listArray);
}
I think you have two instances of your syncFrontEndPref object instantiated.
If you create a Preference Pane project from the template the File's Owner will be the NSPreferencePane. If you've added another entry for the syncFrontEndPref object, you will be creating a second copy of the object, and mainViewDidLoad won't be called in the second one. The timer won't be triggered for that copy of the object and the listArray won't be updated. Try adding a log statement to the init method. If you see that log statement run twice, you have two copies of the object.
If you do have two copies of the object, I'd suggest removing the copy you added to the xib in IB. Change the class of the File's Owner to your syncFrontEndPref class, and connect your bindings to that object.
Does this look something like your current xib file in IB?

How do you make a Menu Bar Item display how many rows there are in a table?

I have a core data table and would like the Menu Bar item to display how many rows there are in the table. I have already created the menu bar item using this code:
-(void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSStatusItem *statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength] retain]; //Create new status item instance
[statusItem setHighlightMode:YES]; //This does something, I'm sure of it.
[statusItem setTitle:[NSString stringWithFormat:#"%C",0xff50]]; //This labels it. You can also use setImage instead to use an icon. That current code will result in a item labeled "p"
[statusItem setEnabled:YES]; //Self explanatory
[statusItem setMenu:theMenu];
[statusItem setToolTip:#"TOOLTIP HA AWESOME AMIRITE?"]; //Optional, just for kicks.
}
What do I need to add to make the Menu Bar item display how many rows there are in the table?
If you don't require live updating you can try this approach:
1) set the delegate of theMenu:
[theMenu setDelegate:self];
2) and implement the delegate methode:
- (void)menuWillOpen:(NSMenu *)menu {
NSUInteger count = [self.tableView numberOfRows];
[[menu itemAtIndex:0] setTitle: [NSString stringWithFormat:#"%d rows", count]];
}
This code will refresh the menu item every time the user opens the menu. If you want to refresh it every time something in the table changes, you will need to use KVO to observe the array controller. You will also need to use KVO if you want to display the count in the title of the StatusItem.

Resources