#import "TemplateWindowController.h" #import "TemplateStream.h" #import "Element.h" #import "ElementOCNT.h" #import "ElementLSTE.h" // and ones for keyed fields #import "ElementDBYT.h" #import "ElementDWRD.h" #import "ElementDLNG.h" #import "NSOutlineView-SelectedItems.h" @implementation TemplateWindowController - (id)initWithResource:(id )newResource { return [self initWithResources:newResource, nil]; } - (id)initWithResources:(id )newResource, ... { id tmplResource; va_list resourceList; va_start(resourceList, newResource); self = [self initWithWindowNibName:@"TemplateWindow"]; if(!self) { va_end(resourceList); return nil; } toolbarItems = [[NSMutableDictionary alloc] init]; // undoManager = [[NSUndoManager alloc] init]; liveEdit = NO; if(liveEdit) { resource = [(id)newResource retain]; // resource to work on backup = [(NSObject *)resource copy]; // for reverting only } else { backup = [(id)newResource retain]; // actual resource to change when saving data resource = [(NSObject *)backup copy]; // resource to work on } templateStructure = [[NSMutableArray alloc] init]; resourceStructure = [[NSMutableArray alloc] init]; tmplResource = va_arg(resourceList, id); [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(templateDataDidChange:) name:ResourceDataDidChangeNotification object:tmplResource]; [self readTemplate:tmplResource]; // reads (but doesn't retain) the template for this resource (TMPL resource with name equal to the passed resource's type) while(tmplResource = va_arg(resourceList, id)) NSLog(@"Too many params passed to -initWithResources:%@", [tmplResource description]); va_end(resourceList); // load the window from the nib [self setShouldCascadeWindows:YES]; [self window]; return self; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; [toolbarItems release]; [templateStructure release]; [resourceStructure release]; [(id)resource release]; [(id)backup release]; [super dealloc]; } /* - (void)windowControllerDidLoadNib:(NSWindowController *)controller { [super windowControllerDidLoadNib:controller]; [self setupToolbar:controller]; } */ - (void)windowDidLoad { [super windowDidLoad]; [self setupToolbar]; [self loadResource]; if(liveEdit) [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resourceDataDidChange:) name:ResourceDataDidChangeNotification object:resource]; else [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resourceDataDidChange:) name:ResourceDataDidChangeNotification object:backup]; [[self window] setTitle:[resource defaultWindowTitle]]; [[[dataList tableColumnWithIdentifier:@"label"] dataCell] setFont:[NSFont boldSystemFontOfSize:[NSFont systemFontSize]]]; [self showWindow:self]; [displayList reloadData]; } - (void)templateDataDidChange:(NSNotification *)notification { [templateStructure removeAllObjects]; [self readTemplate:[notification object]]; if([self isWindowLoaded]) [self loadResource]; } - (void)resourceDataDidChange:(NSNotification *)notification { if(!liveEdit) // bug: should display alert asking if you want to replace data in this editor or reassert this data, revoking the other editor's changes [resource setData:[[backup data] copy]]; [self loadResource]; } - (void)loadResource { // create new stream TemplateStream *stream = [TemplateStream streamWithBytes:(char *)[[resource data] bytes] length:[[resource data] length]]; // loop through template cloning its elements Element *element; [resourceStructure removeAllObjects]; NSEnumerator *enumerator = [templateStructure objectEnumerator]; while(element = [enumerator nextObject]) { Element *clone = [[element copy] autorelease]; // copy the template object. // NSLog(@"clone = %@; resourceStructure = %@", clone, resourceStructure); [resourceStructure addObject:clone]; // add it to our parsed resource data list. Do this right away so the element can append other items should it desire to. [clone setParentArray:resourceStructure]; // the parent for these is the root level resourceStructure object Class cc = [clone class]; BOOL pushedCounter = NO; BOOL pushedKey = NO; if(cc == [ElementOCNT class]) { [stream pushCounter:(ElementOCNT *)clone]; pushedCounter = YES; } if(cc == [ElementKBYT class] || cc == [ElementKWRD class] || cc == [ElementKLNG class] ) { [stream pushKey:clone]; pushedKey = YES; } [clone readDataFrom:stream]; // fill it with resource data. if(cc == [ElementLSTE class] && pushedCounter) [stream popCounter]; // if(cc == [ElementKEYE class] && pushedKey) // [stream popKey]; } // reload the view id item; [dataList reloadData]; int row = [dataList numberOfRows]; while(item = [dataList itemAtRow: --row]) { if([dataList isExpandable: item] && ![dataList isItemExpanded: item]) [dataList expandItem: item expandChildren: YES]; } } - (BOOL)windowShouldClose:(id)sender { [[self window] makeFirstResponder:dataList]; [dataList abortEditing]; if([[self window] isDocumentEdited]) { NSBundle *bundle = [NSBundle bundleForClass:[self class]]; NSBeginAlertSheet(NSLocalizedStringFromTableInBundle(@"KeepChangesDialogTitle", nil, bundle, nil), NSLocalizedStringFromTableInBundle(@"KeepChangesButton", nil, bundle, nil), NSLocalizedStringFromTableInBundle(@"DiscardChangesButton", nil, bundle, nil), NSLocalizedStringFromTableInBundle(@"CancelButton", nil, bundle, nil), sender, self, @selector(saveSheetDidClose:returnCode:contextInfo:), nil, nil, NSLocalizedStringFromTableInBundle(@"KeepChangesDialogMessage", nil, bundle, nil)); return NO; } else return YES; } - (void)saveSheetDidClose:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo { switch(returnCode) { case NSAlertDefaultReturn: // keep [self saveResource:nil]; [[self window] close]; break; case NSAlertAlternateReturn: // don't keep [[self window] close]; break; case NSAlertOtherReturn: // cancel break; } } - (void)saveResource:(id)sender { // get size of resource by summing size of all fields Element *element; unsigned int size = 0; NSEnumerator *enumerator = [resourceStructure objectEnumerator]; while(element = [enumerator nextObject]) size += [element sizeOnDisk]; // create data and stream NSMutableData *newData = [NSMutableData dataWithLength:size]; TemplateStream *stream = [TemplateStream streamWithBytes:(char *)[newData bytes] length:size]; // write bytes into new data object enumerator = [resourceStructure objectEnumerator]; while(element = [enumerator nextObject]) [element writeDataTo:stream]; // send the new resource data to ResKnife if(liveEdit) { [[NSNotificationCenter defaultCenter] removeObserver:self name:ResourceDataDidChangeNotification object:resource]; [resource setData:newData]; [backup setData:[newData copy]]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resourceDataDidChange:) name:ResourceDataDidChangeNotification object:resource]; } else { [[NSNotificationCenter defaultCenter] removeObserver:self name:ResourceDataDidChangeNotification object:backup]; [resource setData:newData]; [backup setData:[newData copy]]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resourceDataDidChange:) name:ResourceDataDidChangeNotification object:backup]; [self setDocumentEdited:NO]; } } - (void)revertResource:(id)sender { [resource setData:[[backup data] copy]]; } - (void)readTemplate:(id)tmplRes { char *data = (char*) [[tmplRes data] bytes]; unsigned long bytesToGo = [[tmplRes data] length]; TemplateStream *stream = [TemplateStream streamWithBytes:data length:bytesToGo]; // read new fields from the template and add them to our list while([stream bytesToGo] > 0) { Element *element = [stream readOneElement]; if(element) { [element setIsTMPL:YES]; // for debugging [templateStructure addObject:element]; } else { NSLog(@"Error reading template stream, aborting."); break; } } [displayList reloadData]; } #pragma mark - #pragma mark Table Management - (id)outlineView:(NSOutlineView*)outlineView child:(int)index ofItem:(id)item { if((item == nil) && (outlineView == displayList)) return [templateStructure objectAtIndex:index]; else if((item == nil) && (outlineView == dataList)) return [resourceStructure objectAtIndex:index]; else return [item subElementAtIndex:index]; } - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item { return ([item subElementCount] > 0); } - (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item { if((item == nil) && (outlineView == displayList)) return [templateStructure count]; else if((item == nil) && (outlineView == dataList)) return [resourceStructure count]; else return [item subElementCount]; } - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item { return [item valueForKey:[tableColumn identifier]]; } - (void)outlineView:(NSOutlineView *)outlineView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn byItem:(id)item { id old = [item valueForKey:[tableColumn identifier]]; if([(Element *)item editable] && ![old isEqual:object]) { // [[self undoManager] registerUndoWithTarget:item selector:@selector(setStringValue:) object:old]; // [[self undoManager] setActionName:NSLocalizedString(@"Changes", nil)]; [item setValue:object forKey:[tableColumn identifier]]; if(!liveEdit) [self setDocumentEdited:YES]; // remove self to avoid reloading the resource [[NSNotificationCenter defaultCenter] removeObserver:self name:ResourceDataDidChangeNotification object:resource]; [[NSNotificationCenter defaultCenter] postNotificationName:ResourceDataDidChangeNotification object:resource]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resourceDataDidChange:) name:ResourceDataDidChangeNotification object:resource]; } } - (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item { return [(Element *)item editable]; } /*- (float)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item { return [item rowHeight]; }*/ #pragma mark - #pragma mark Menu Management // these next five methods are a crude hack - the items ought to be in the responder chain themselves - (IBAction)createListEntry:(id)sender; { // This works by selecting an item that serves as a template (another LSTB), or knows how to create an item (LSTE) and passing the message on to it. id element = [dataList selectedItem]; if([element respondsToSelector:@selector(createListEntry:)]) { [element createListEntry:sender]; [dataList reloadData]; [dataList expandItem:[dataList selectedItem] expandChildren:YES]; if(!liveEdit) [self setDocumentEdited:YES]; } } - (IBAction)cut:(id)sender; { [[dataList selectedItem] cut:sender]; [dataList reloadData]; if(!liveEdit) [self setDocumentEdited:YES]; } - (IBAction)copy:(id)sender; { [[dataList selectedItem] copy:sender]; [dataList reloadData]; } - (IBAction)paste:(id)sender; { [[dataList selectedItem] paste:sender]; [dataList reloadData]; if(!liveEdit) [self setDocumentEdited:YES]; } - (IBAction)clear:(id)sender; { [[dataList selectedItem] clear:sender]; [dataList reloadData]; if(!liveEdit) [self setDocumentEdited:YES]; } - (BOOL)validateMenuItem:(NSMenuItem*)item { Element *element = (Element*) [dataList selectedItem]; if([item action] == @selector(createListEntry:)) return(element && [element respondsToSelector:@selector(createListEntry:)]); else if([item action] == @selector(cut:)) return(element && [element respondsToSelector:@selector(cut:)]); else if([item action] == @selector(copy:)) return(element && [element respondsToSelector:@selector(copy:)]); else if([item action] == @selector(paste:) && element && [element respondsToSelector:@selector(validateMenuItem:)]) return([element validateMenuItem:item]); else if([item action] == @selector(clear:)) return(element && [element respondsToSelector:@selector(clear:)]); else if([item action] == @selector(saveDocument:)) return YES; else return NO; } - (void)windowDidBecomeKey:(NSNotification *)notification { NSMenu *resourceMenu = [[[NSApp mainMenu] itemAtIndex:3] submenu]; NSMenuItem *createItem = [resourceMenu itemAtIndex:[resourceMenu indexOfItemWithTarget:nil andAction:@selector(showCreateResourceSheet:)]]; [createItem setTitle:NSLocalizedString(@"Create List Entry", nil)]; [createItem setAction:@selector(createListEntry:)]; } - (void)windowDidResignKey:(NSNotification *)notification { NSMenu *resourceMenu = [[[NSApp mainMenu] itemAtIndex:3] submenu]; NSMenuItem *createItem = [resourceMenu itemAtIndex:[resourceMenu indexOfItemWithTarget:nil andAction:@selector(createListEntry:)]]; [createItem setTitle:NSLocalizedString(@"Create New Resource...", nil)]; [createItem setAction:@selector(showCreateResourceSheet:)]; } #pragma mark - #pragma mark Toolbar Management static NSString *RKTEToolbarIdentifier = @"com.nickshanks.resknife.templateeditor.toolbar"; static NSString *RKTEDisplayTMPLIdentifier = @"com.nickshanks.resknife.templateeditor.toolbar.tmpl"; - (void)setupToolbar { /* This routine should become invalid once toolbars are integrated into nib files */ NSToolbarItem *item; [toolbarItems removeAllObjects]; // just in case this method is called more than once per document (which it shouldn't be!) item = [[[NSToolbarItem alloc] initWithItemIdentifier:RKTEDisplayTMPLIdentifier] autorelease]; [item setLabel:NSLocalizedString(@"Parsed TMPL", nil)]; [item setPaletteLabel:NSLocalizedString(@"Display Parsed TMPL", nil)]; [item setToolTip:NSLocalizedString(@"Display Parsed TMPL", nil)]; [item setImage:[NSImage imageNamed:@"DisplayTMPL"]]; [item setTarget:tmplDrawer]; [item setAction:@selector(toggle:)]; [toolbarItems setObject:item forKey:RKTEDisplayTMPLIdentifier]; NSToolbar *toolbar = [[[NSToolbar alloc] initWithIdentifier:RKTEToolbarIdentifier] autorelease]; // set toolbar properties [toolbar setVisible:NO]; [toolbar setAutosavesConfiguration:YES]; [toolbar setAllowsUserCustomization:YES]; [toolbar setDisplayMode:NSToolbarDisplayModeLabelOnly]; [toolbar setSizeMode:NSToolbarSizeModeSmall]; // attach toolbar to window [toolbar setDelegate:self]; [[self window] setToolbar:toolbar]; } - (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag { return [toolbarItems objectForKey:itemIdentifier]; } - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar { return [NSArray arrayWithObjects:RKTEDisplayTMPLIdentifier, NSToolbarFlexibleSpaceItemIdentifier, NSToolbarPrintItemIdentifier, nil]; } - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar { return [NSArray arrayWithObjects:RKTEDisplayTMPLIdentifier, NSToolbarPrintItemIdentifier, NSToolbarCustomizeToolbarItemIdentifier, NSToolbarFlexibleSpaceItemIdentifier, NSToolbarSpaceItemIdentifier, NSToolbarSeparatorItemIdentifier, nil]; } @end #pragma mark - #pragma mark Table Event Handling @implementation NTOutlineView - (void)keyDown:(NSEvent *)event { Element *selectedItem = nil; int selectedRow = [self selectedRow]; if(selectedRow != -1) selectedItem = [self selectedItem]; if(selectedItem && [selectedItem editable] && ([[event characters] isEqualToString:@"\r"] || [[event characters] isEqualToString:@"\t"])) [self editColumn:1 row:selectedRow withEvent:nil select:YES]; else if(selectedItem && [selectedItem respondsToSelector:@selector(clear:)] && [[event characters] isEqualToString:[NSString stringWithCString:"\x7F"]]) [[[self window] windowController] clear:nil]; else [super keyDown:event]; } - (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)selector { // pressed return, end editing if(selector == @selector(insertNewline:)) { [[self window] makeFirstResponder:self]; [self abortEditing]; return YES; } return [super textView:textView doCommandBySelector:selector]; } @end