ResKnife/Cocoa/Plug-Ins/Template Editor/TemplateWindowController.m

462 lines
16 KiB
Objective-C

#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 <ResKnifeResourceProtocol>)newResource
{
return [self initWithResources:newResource, nil];
}
- (id)initWithResources:(id <ResKnifeResourceProtocol>)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<ResKnifeResourceProtocol>)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