#import #import #import "JVChatWindowController.h" #import "MVConnectionsController.h" #import "JVChatController.h" #import "JVChatRoom.h" #import "JVChatRoomBrowser.h" #import "JVDirectChat.h" #import "JVDetailCell.h" #import "MVMenuButton.h" NSString *JVToolbarToggleChatDrawerItemIdentifier = @"JVToolbarToggleChatDrawerItem"; NSString *JVToolbarToggleChatActivityItemIdentifier = @"JVToolbarToggleChatActivityItem"; NSString *JVChatViewPboardType = @"Colloquy Chat View v1.0 pasteboard type"; @interface NSToolbar (NSToolbarPrivate) - (NSView *) _toolbarView; @end #pragma mark - @interface JVChatWindowController (JVChatWindowControllerPrivate) - (void) _claimMenuCommands; - (void) _resignMenuCommands; - (void) _refreshSelectionMenu; - (void) _refreshWindow; - (void) _refreshWindowTitle; - (void) _refreshList; @end #pragma mark - @interface NSOutlineView (ASEntendedOutlineView) - (void) redisplayItemEqualTo:(id) item; @end #pragma mark - @implementation JVChatWindowController - (id) init { return ( self = [self initWithWindowNibName:@"JVChatWindow"] ); } - (id) initWithWindowNibName:(NSString *) windowNibName { if( ( self = [super initWithWindowNibName:windowNibName] ) ) { viewsDrawer = nil; chatViewsOutlineView = nil; viewActionButton = nil; favoritesButton = nil; _activityToolbarItem = nil; _activeViewController = nil; _views = [[NSMutableArray array] retain]; _usesSmallIcons = [[NSUserDefaults standardUserDefaults] boolForKey:@"JVChatWindowUseSmallDrawerIcons"]; } return self; } - (void) windowDidLoad { _placeHolder = [[[self window] contentView] retain]; NSTableColumn *column = [chatViewsOutlineView outlineTableColumn]; JVDetailCell *prototypeCell = [[JVDetailCell new] autorelease]; [prototypeCell setFont:[NSFont toolTipsFontOfSize:11.]]; [column setDataCell:prototypeCell]; [chatViewsOutlineView setRefusesFirstResponder:YES]; [chatViewsOutlineView setDoubleAction:@selector( _doubleClickedListItem: )]; [chatViewsOutlineView setAutoresizesOutlineColumn:YES]; [chatViewsOutlineView setMenu:[[[NSMenu alloc] initWithTitle:@""] autorelease]]; [chatViewsOutlineView registerForDraggedTypes:[NSArray arrayWithObjects:JVChatViewPboardType, NSFilenamesPboardType, nil]]; [favoritesButton setMenu:[MVConnectionsController favoritesMenu]]; [[self window] setFrameUsingName:@"Chat Window"]; [[self window] setFrameAutosaveName:@"Chat Window"]; [[self window] setOpaque:NO]; // let us poke transparant holes in the window NSSize drawerSize = NSSizeFromString( [[NSUserDefaults standardUserDefaults] stringForKey:@"JVChatWindowDrawerSize"] ); if( drawerSize.width ) [viewsDrawer setContentSize:drawerSize]; if( [[NSUserDefaults standardUserDefaults] boolForKey:@"JVChatWindowDrawerOpen"] ) [viewsDrawer open:nil]; [self _refreshList]; } - (void) dealloc { [[self window] setDelegate:nil]; [[self window] setToolbar:nil]; [[self window] setContentView:_placeHolder]; [viewsDrawer setDelegate:nil]; [chatViewsOutlineView setDelegate:nil]; [chatViewsOutlineView setDataSource:nil]; [favoritesButton setMenu:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self]; NSEnumerator *enumerator = [_views objectEnumerator]; id controller = nil; while( ( controller = [enumerator nextObject] ) ) [controller setWindowController:nil]; [_placeHolder release]; [_activeViewController release]; [_activityToolbarItem release]; [_views release]; _placeHolder = nil; _activityToolbarItem = nil; _activeViewController = nil; _views = nil; [super dealloc]; } #pragma mark - - (BOOL) respondsToSelector:(SEL) selector { if( [_activeViewController respondsToSelector:selector] ) return [_activeViewController respondsToSelector:selector]; else return [super respondsToSelector:selector]; } - (void) forwardInvocation:(NSInvocation *) invocation { if( [_activeViewController respondsToSelector:[invocation selector]] ) [invocation invokeWithTarget:_activeViewController]; else [super forwardInvocation:invocation]; } - (NSMethodSignature *) methodSignatureForSelector:(SEL) selector { if( [_activeViewController respondsToSelector:selector] ) return [(NSObject *)_activeViewController methodSignatureForSelector:selector]; else return [super methodSignatureForSelector:selector]; } #pragma mark - - (void) showChatViewController:(id ) controller { NSAssert1( [_views containsObject:controller], @"%@ is not a member of this window controller.", controller ); [chatViewsOutlineView selectRow:[chatViewsOutlineView rowForItem:controller] byExtendingSelection:NO]; [chatViewsOutlineView scrollRowToVisible:[chatViewsOutlineView rowForItem:controller]]; [self _refreshList]; [self _refreshWindow]; } #pragma mark - - (id ) objectToInspect { id item = [self selectedListItem]; if( [item conformsToProtocol:@protocol( JVInspection )] ) return item; else return nil; } - (IBAction) getInfo:(id) sender { id item = [self selectedListItem]; if( [item conformsToProtocol:@protocol( JVInspection )] ) if( [[[NSApplication sharedApplication] currentEvent] modifierFlags] & NSAlternateKeyMask ) [JVInspectorController showInspector:sender]; else [[JVInspectorController inspectorOfObject:item] show:sender]; } #pragma mark - - (IBAction) joinRoom:(id) sender { [[JVChatRoomBrowser chatRoomBrowserForConnection:[_activeViewController connection]] showWindow:nil]; } #pragma mark - - (void) close { [[JVChatController defaultManager] performSelector:@selector( disposeChatWindowController: ) withObject:self afterDelay:0.]; [[self window] orderOut:nil]; [super close]; } - (IBAction) closeCurrentPanel:(id) sender { [[JVChatController defaultManager] disposeViewController:_activeViewController]; } - (IBAction) detachCurrentPanel:(id) sender { [[JVChatController defaultManager] detachViewController:_activeViewController]; } - (IBAction) selectPreviousPanel:(id) sender { int currentIndex = [_views indexOfObject:_activeViewController]; int index = 0; if( currentIndex - 1 >= 0 ) index = ( currentIndex - 1 ); else index = ( [_views count] - 1 ); [self showChatViewController:[_views objectAtIndex:index]]; } - (IBAction) selectPreviousActivePanel:(id) sender { int currentIndex = [_views indexOfObject:_activeViewController]; int index = currentIndex; BOOL done = NO; do { if( [[_views objectAtIndex:index] respondsToSelector:@selector( newMessagesWaiting )] && [[_views objectAtIndex:index] newMessagesWaiting] > 0 ) done = YES; if( ! done ) { if( index == 0 ) index = [_views count] - 1; else index--; } } while( index != currentIndex && ! done ); [self showChatViewController:[_views objectAtIndex:index]]; } - (IBAction) selectNextPanel:(id) sender { int currentIndex = [_views indexOfObject:_activeViewController]; int index = 0; if( currentIndex + 1 < [_views count] ) index = ( currentIndex + 1 ); else index = 0; [self showChatViewController:[_views objectAtIndex:index]]; } - (IBAction) selectNextActivePanel:(id) sender { int currentIndex = [_views indexOfObject:_activeViewController]; int index = currentIndex; BOOL done = NO; do { if( [[_views objectAtIndex:index] respondsToSelector:@selector( newMessagesWaiting )] && [[_views objectAtIndex:index] newMessagesWaiting] > 0 ) done = YES; if( ! done ) { if( index == [_views count] - 1 ) index = 0; else index++; } } while( index != currentIndex && ! done ); [self showChatViewController:[_views objectAtIndex:index]]; } #pragma mark - - (id ) activeChatViewController { return [[_activeViewController retain] autorelease]; } - (id ) selectedListItem { long index = -1; if( ( index = [chatViewsOutlineView selectedRow] ) == -1 ) return nil; return [chatViewsOutlineView itemAtRow:index]; } #pragma mark - - (void) addChatViewController:(id ) controller { [self insertChatViewController:controller atIndex:[_views count]]; } - (void) insertChatViewController:(id ) controller atIndex:(unsigned int) index { NSParameterAssert( controller != nil ); NSAssert1( ! [_views containsObject:controller], @"%@ already added.", controller ); NSAssert( index >= 0 && index <= [_views count], @"Index is beyond bounds." ); [_views insertObject:controller atIndex:index]; [controller setWindowController:self]; [self _refreshList]; [self _refreshWindow]; if( [self isMemberOfClass:[JVChatWindowController class]] && [_views count] >= 2 ) { [chatViewsOutlineView scrollRowToVisible:[chatViewsOutlineView rowForItem:controller]]; [viewsDrawer open]; } } #pragma mark - - (void) removeChatViewController:(id ) controller { NSParameterAssert( controller != nil ); NSAssert1( [_views containsObject:controller], @"%@ is not a member of this window controller.", controller ); if( _activeViewController == controller ) { [_activeViewController autorelease]; _activeViewController = nil; } [controller setWindowController:nil]; [_views removeObjectIdenticalTo:controller]; [self _refreshList]; [self _refreshWindow]; if( ! [_views count] && [[self window] isVisible] ) [self close]; } - (void) removeChatViewControllerAtIndex:(unsigned int) index { NSAssert( index >= 0 && index <= [_views count], @"Index is beyond bounds." ); [self removeChatViewController:[_views objectAtIndex:index]]; } - (void) removeAllChatViewControllers { [_activeViewController autorelease]; _activeViewController = nil; NSEnumerator *enumerator = [_views objectEnumerator]; id controller = nil; while( ( controller = [enumerator nextObject] ) ) [controller setWindowController:nil]; [_views removeAllObjects]; [self _refreshList]; [self _refreshWindow]; if( [[self window] isVisible] ) [self close]; } #pragma mark - - (void) replaceChatViewController:(id ) controller withController:(id ) newController { NSParameterAssert( controller != nil ); NSParameterAssert( newController != nil ); NSAssert1( [_views containsObject:controller], @"%@ is not a member of this window controller.", controller ); NSAssert1( ! [_views containsObject:newController], @"%@ is already a member of this window controller.", newController ); [self replaceChatViewControllerAtIndex:[_views indexOfObjectIdenticalTo:controller] withController:newController]; } - (void) replaceChatViewControllerAtIndex:(unsigned int) index withController:(id ) controller { id oldController = nil; NSParameterAssert( controller != nil ); NSAssert1( ! [_views containsObject:controller], @"%@ is already a member of this window controller.", controller ); NSAssert( index >= 0 && index <= [_views count], @"Index is beyond bounds." ); oldController = [_views objectAtIndex:index]; if( _activeViewController == oldController ) { [_activeViewController autorelease]; _activeViewController = nil; } [oldController setWindowController:nil]; [_views replaceObjectAtIndex:index withObject:controller]; [controller setWindowController:self]; [self _refreshList]; [self _refreshWindow]; } #pragma mark - - (NSArray *) chatViewControllersForConnection:(MVChatConnection *) connection { NSMutableArray *ret = [NSMutableArray array]; NSEnumerator *enumerator = [_views objectEnumerator]; id controller = nil; NSParameterAssert( connection != nil ); while( ( controller = [enumerator nextObject] ) ) if( [controller connection] == connection ) [ret addObject:controller]; return [[ret retain] autorelease]; } - (NSArray *) chatViewControllersWithControllerClass:(Class) class { NSMutableArray *ret = [NSMutableArray array]; NSEnumerator *enumerator = [_views objectEnumerator]; id controller = nil; NSParameterAssert( class != NULL ); NSAssert( [class conformsToProtocol:@protocol( JVChatViewController )], @"The tab controller class must conform to the JVChatViewController protocol." ); ret = [NSMutableArray array]; while( ( controller = [enumerator nextObject] ) ) if( [controller isMemberOfClass:class] ) [ret addObject:controller]; return [[ret retain] autorelease]; } - (NSArray *) allChatViewControllers { return [[_views retain] autorelease]; } #pragma mark - - (NSToolbarItem *) toggleChatDrawerToolbarItem { NSToolbarItem *toolbarItem = [[[NSToolbarItem alloc] initWithItemIdentifier:JVToolbarToggleChatDrawerItemIdentifier] autorelease]; [toolbarItem setLabel:NSLocalizedString( @"Drawer", "chat panes drawer toolbar item name" )]; [toolbarItem setPaletteLabel:NSLocalizedString( @"Panel Drawer", "chat panes drawer toolbar customize palette name" )]; [toolbarItem setToolTip:NSLocalizedString( @"Toggle Chat Panel Drawer", "chat panes drawer toolbar item tooltip" )]; [toolbarItem setImage:[NSImage imageNamed:@"showdrawer"]]; [toolbarItem setTarget:self]; [toolbarItem setAction:@selector( toggleViewsDrawer: )]; return toolbarItem; } - (IBAction) toggleViewsDrawer:(id) sender { [viewsDrawer toggle:sender]; if( [viewsDrawer state] == NSDrawerClosedState || [viewsDrawer state] == NSDrawerClosingState ) [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"JVChatWindowDrawerOpen"]; else if( [viewsDrawer state] == NSDrawerOpenState || [viewsDrawer state] == NSDrawerOpeningState ) [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"JVChatWindowDrawerOpen"]; } - (IBAction) openViewsDrawer:(id) sender { [viewsDrawer open:sender]; [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"JVChatWindowDrawerOpen"]; } - (IBAction) closeViewsDrawer:(id) sender { [viewsDrawer close:sender]; [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"JVChatWindowDrawerOpen"]; } - (IBAction) toggleSmallDrawerIcons:(id) sender { _usesSmallIcons = ! _usesSmallIcons; [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:_usesSmallIcons] forKey:@"JVChatWindowUseSmallDrawerIcons"]; [self _refreshList]; } #pragma mark - - (void) reloadListItem:(id ) item andChildren:(BOOL) children { id selectItem = [self selectedListItem]; [chatViewsOutlineView reloadItem:item reloadChildren:( children && [chatViewsOutlineView isItemExpanded:item] ? YES : NO )]; if( _activeViewController == item ) [self _refreshWindowTitle]; if( [self isMemberOfClass:[JVChatWindowController class]] && [[NSUserDefaults standardUserDefaults] boolForKey:@"JVKeepActiveDrawerPanelsVisible"] && [item isKindOfClass:[JVDirectChat class]] && [(id)item newMessagesWaiting] ) { NSRange visibleRows = [chatViewsOutlineView rowsInRect:[chatViewsOutlineView visibleRect]]; int row = [chatViewsOutlineView rowForItem:item]; if( ! NSLocationInRange( row, visibleRows ) && row > 0 ) { int index = [_views indexOfObjectIdenticalTo:item]; row = ( index > row ? NSMaxRange( visibleRows ) : visibleRows.location + 1 ); id rowItem = [chatViewsOutlineView itemAtRow:row]; // this will break if the list has more than 2 levels if( [chatViewsOutlineView levelForRow:row] > 0 ) rowItem = [rowItem parent]; if( rowItem ) row = [_views indexOfObjectIdenticalTo:rowItem]; if( rowItem && row != NSNotFound ) { [item retain]; [_views removeObjectAtIndex:index]; [_views insertObject:item atIndex:( index > row || ! row ? row : row - 1 )]; [item release]; [chatViewsOutlineView reloadData]; } } } if( item == selectItem ) [self _refreshSelectionMenu]; if( selectItem ) { int selectedRow = [chatViewsOutlineView rowForItem:selectItem]; [chatViewsOutlineView selectRow:selectedRow byExtendingSelection:NO]; } } - (BOOL) isListItemExpanded:(id ) item { return [chatViewsOutlineView isItemExpanded:item]; } - (void) expandListItem:(id ) item { [chatViewsOutlineView expandItem:item]; } - (void) collapseListItem:(id ) item { [chatViewsOutlineView collapseItem:item]; } #pragma mark - - (BOOL) validateMenuItem:(NSMenuItem *) menuItem { if( [menuItem action] == @selector( toggleSmallDrawerIcons: ) ) { [menuItem setState:( _usesSmallIcons ? NSOnState : NSOffState )]; return YES; } else if( [menuItem action] == @selector( toggleViewsDrawer: ) ) { if( [viewsDrawer state] == NSDrawerClosedState || [viewsDrawer state] == NSDrawerClosingState ) { [menuItem setTitle:[NSString stringWithFormat:NSLocalizedString( @"Show Drawer", "show drawer menu title" )]]; } else { [menuItem setTitle:[NSString stringWithFormat:NSLocalizedString( @"Hide Drawer", "hide drawer menu title" )]]; } return YES; } else if( [menuItem action] == @selector( getInfo: ) ) { id item = [self selectedListItem]; if( [item conformsToProtocol:@protocol( JVInspection )] ) return YES; else return NO; } else if( [menuItem action] == @selector( closeCurrentPanel: ) ) { if( [[menuItem keyEquivalent] length] ) return YES; else return NO; } return YES; } @end #pragma mark - @implementation JVChatWindowController (JVChatWindowControllerDelegate) - (NSSize) drawerWillResizeContents:(NSDrawer *) drawer toSize:(NSSize) contentSize { [[NSUserDefaults standardUserDefaults] setObject:NSStringFromSize( contentSize ) forKey:@"JVChatWindowDrawerSize"]; return contentSize; } #pragma mark - - (void) windowWillClose:(NSNotification *) notification { if( ! [[[[[NSApplication sharedApplication] keyWindow] windowController] className] isEqual:[self className]] ) [self _resignMenuCommands]; if( [[self window] isVisible] ) [self close]; } - (void) windowDidResignKey:(NSNotification *) notification { if( ! [[[[[NSApplication sharedApplication] keyWindow] windowController] className] isEqual:[self className]] ) [self _resignMenuCommands]; } - (void) windowDidResignMain:(NSNotification *) notification { if( ! [[[[[NSApplication sharedApplication] keyWindow] windowController] className] isEqual:[self className]] ) [self _resignMenuCommands]; } - (void) windowDidBecomeMain:(NSNotification *) notification { [self _claimMenuCommands]; if( _activeViewController ) { [[self window] makeFirstResponder:[_activeViewController firstResponder]]; [self reloadListItem:_activeViewController andChildren:NO]; } } - (void) windowDidBecomeKey:(NSNotification *) notification { [self _claimMenuCommands]; if( _activeViewController ) { [[self window] makeFirstResponder:[_activeViewController firstResponder]]; [self reloadListItem:_activeViewController andChildren:NO]; } } #pragma mark - - (void) outlineView:(NSOutlineView *) outlineView willDisplayCell:(id) cell forTableColumn:(NSTableColumn *) tableColumn item:(id) item { [(JVDetailCell *) cell setRepresentedObject:item]; [(JVDetailCell *) cell setMainText:[item title]]; if( [item respondsToSelector:@selector( information )] ) { [(JVDetailCell *) cell setInformationText:[item information]]; } else [(JVDetailCell *) cell setInformationText:nil]; if( [item respondsToSelector:@selector( statusImage )] ) { [(JVDetailCell *) cell setStatusImage:[item statusImage]]; } else [(JVDetailCell *) cell setStatusImage:nil]; if( [item respondsToSelector:@selector( isEnabled )] ) { [cell setEnabled:[item isEnabled]]; } else [cell setEnabled:YES]; } - (NSString *) outlineView:(NSOutlineView *) outlineView toolTipForItem:(id) item inTrackingRect:(NSRect) rect forCell:(id) cell { if( [item respondsToSelector:@selector( toolTip )] ) return [item toolTip]; return nil; } - (int) outlineView:(NSOutlineView *) outlineView numberOfChildrenOfItem:(id) item { if( item && [item respondsToSelector:@selector( numberOfChildren )] ) return [item numberOfChildren]; else return [_views count]; } - (BOOL) outlineView:(NSOutlineView *) outlineView isItemExpandable:(id) item { return ( [item respondsToSelector:@selector( numberOfChildren )] && [item numberOfChildren] ? YES : NO ); } - (id) outlineView:(NSOutlineView *) outlineView child:(int) index ofItem:(id) item { if( item ) { if( [item respondsToSelector:@selector( childAtIndex: )] ) return [item childAtIndex:index]; else return nil; } else return [_views objectAtIndex:index]; } - (id) outlineView:(NSOutlineView *) outlineView objectValueForTableColumn:(NSTableColumn *) tableColumn byItem:(id) item { float maxSideSize = ( ( _usesSmallIcons || [outlineView levelForRow:[outlineView rowForItem:item]] ) ? 16. : 32. ); NSImage *org = [item icon]; if( [org size].width > maxSideSize || [org size].height > maxSideSize ) { NSImage *ret = [[[item icon] copy] autorelease]; [ret setScalesWhenResized:YES]; [ret setSize:NSMakeSize( maxSideSize, maxSideSize )]; org = ret; } return org; } - (BOOL) outlineView:(NSOutlineView *) outlineView shouldEditTableColumn:(NSTableColumn *) tableColumn item:(id) item { return NO; } - (BOOL) outlineView:(NSOutlineView *) outlineView shouldExpandItem:(id) item { if( [[[NSApplication sharedApplication] currentEvent] type] == NSLeftMouseDragged ) return NO; // if we are dragging don't expand return YES; } - (BOOL) outlineView:(NSOutlineView *) outlineView shouldCollapseItem:(id) item { if( [self selectedListItem] != [self activeChatViewController] ) [outlineView selectRow:[outlineView rowForItem:[self activeChatViewController]] byExtendingSelection:NO]; return YES; } - (int) outlineView:(NSOutlineView *) outlineView heightOfRow:(int) row { return ( [outlineView levelForRow:row] || _usesSmallIcons ? 18 : 36 ); } - (void) outlineViewSelectionDidChange:(NSNotification *) notification { id item = [self selectedListItem]; [[JVInspectorController sharedInspector] inspectObject:[self objectToInspect]]; if( [item conformsToProtocol:@protocol( JVChatViewController )] && item != (id) _activeViewController ) [self _refreshWindow]; [self _refreshSelectionMenu]; } - (BOOL) outlineView:(NSOutlineView *) outlineView writeItems:(NSArray *) items toPasteboard:(NSPasteboard *) board { id item = [items lastObject]; NSData *data = [NSData dataWithBytes:&item length:sizeof( &item )]; if( ! [item conformsToProtocol:@protocol( JVChatViewController )] ) return NO; [board declareTypes:[NSArray arrayWithObjects:JVChatViewPboardType, nil] owner:self]; [board setData:data forType:JVChatViewPboardType]; return YES; } - (NSDragOperation) outlineView:(NSOutlineView *) outlineView validateDrop:(id ) info proposedItem:(id) item proposedChildIndex:(int) index { if( [[info draggingPasteboard] availableTypeFromArray:[NSArray arrayWithObject:NSFilenamesPboardType]] ) { if( [item respondsToSelector:@selector( acceptsDraggedFileOfType: )] ) { NSArray *files = [[info draggingPasteboard] propertyListForType:NSFilenamesPboardType]; NSEnumerator *enumerator = [files objectEnumerator]; id file = nil; while( ( file = [enumerator nextObject] ) ) if( [item acceptsDraggedFileOfType:[file pathExtension]] ) return NSDragOperationMove; return NSDragOperationNone; } else return NSDragOperationNone; } else if( [[info draggingPasteboard] availableTypeFromArray:[NSArray arrayWithObject:JVChatViewPboardType]] ) { if( ! item ) return NSDragOperationMove; else return NSDragOperationNone; } else return NSDragOperationNone; } - (BOOL) outlineView:(NSOutlineView *) outlineView acceptDrop:(id ) info item:(id) item childIndex:(int) index { NSPasteboard *board = [info draggingPasteboard]; if( [board availableTypeFromArray:[NSArray arrayWithObject:NSFilenamesPboardType]] ) { NSArray *files = [[info draggingPasteboard] propertyListForType:NSFilenamesPboardType]; NSEnumerator *enumerator = [files objectEnumerator]; id file = nil; if( ! [item respondsToSelector:@selector( acceptsDraggedFileOfType: )] || ! [item respondsToSelector:@selector( handleDraggedFile: )] ) return NO; while( ( file = [enumerator nextObject] ) ) if( [item acceptsDraggedFileOfType:[file pathExtension]] ) [item handleDraggedFile:file]; return YES; } else if( [board availableTypeFromArray:[NSArray arrayWithObject:JVChatViewPboardType]] ) { NSData *pointerData = [board dataForType:JVChatViewPboardType]; id dragedController = nil; [pointerData getBytes:&dragedController]; [[dragedController retain] autorelease]; if( [_views containsObject:dragedController] ) { if( index != NSOutlineViewDropOnItemIndex && index >= [_views indexOfObjectIdenticalTo:dragedController] ) index--; [_views removeObjectIdenticalTo:dragedController]; } else { [[dragedController windowController] removeChatViewController:dragedController]; } if( index == NSOutlineViewDropOnItemIndex ) [self addChatViewController:dragedController]; else [self insertChatViewController:dragedController atIndex:index]; return YES; } return NO; } - (void) outlineViewItemDidCollapse:(NSNotification *) notification { [chatViewsOutlineView sizeLastColumnToFit]; } - (void) outlineViewItemDidExpand:(NSNotification *) notification { [chatViewsOutlineView sizeLastColumnToFit]; } @end #pragma mark - @implementation JVChatWindowController (JVChatWindowControllerPrivate) - (void) _claimMenuCommands { NSMenuItem *closeItem = [[[[[NSApplication sharedApplication] mainMenu] itemAtIndex:1] submenu] itemWithTag:1]; [closeItem setKeyEquivalentModifierMask:NSCommandKeyMask]; [closeItem setKeyEquivalent:@"W"]; closeItem = (NSMenuItem *)[[[[[NSApplication sharedApplication] mainMenu] itemAtIndex:1] submenu] itemWithTag:2]; [closeItem setKeyEquivalentModifierMask:NSCommandKeyMask]; [closeItem setKeyEquivalent:@"w"]; } - (void) _resignMenuCommands { NSMenuItem *closeItem = [[[[[NSApplication sharedApplication] mainMenu] itemAtIndex:1] submenu] itemWithTag:1]; [closeItem setKeyEquivalentModifierMask:NSCommandKeyMask]; [closeItem setKeyEquivalent:@"w"]; closeItem = (NSMenuItem *)[[[[[NSApplication sharedApplication] mainMenu] itemAtIndex:1] submenu] itemWithTag:2]; [closeItem setKeyEquivalentModifierMask:0]; [closeItem setKeyEquivalent:@""]; } - (IBAction) _doubleClickedListItem:(id) sender { id item = [self selectedListItem]; if( [item respondsToSelector:@selector( doubleClicked: )] ) [item doubleClicked:sender]; } - (void) _refreshSelectionMenu { id item = [self selectedListItem]; if( ! item ) item = [self activeChatViewController]; id menuItem = nil; NSMenu *menu = [chatViewsOutlineView menu]; NSMenu *newMenu = ( [item respondsToSelector:@selector( menu )] ? [item menu] : nil ); NSEnumerator *enumerator = [[[[menu itemArray] copy] autorelease] objectEnumerator]; while( ( menuItem = [enumerator nextObject] ) ) [menu removeItem:menuItem]; enumerator = [[[[newMenu itemArray] copy] autorelease] objectEnumerator]; while( ( menuItem = [enumerator nextObject] ) ) { [newMenu removeItem:menuItem]; [menu addItem:menuItem]; } NSMethodSignature *signature = [NSMethodSignature methodSignatureWithReturnAndArgumentTypes:@encode( NSArray * ), @encode( id ), @encode( id ), nil]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; id view = [item parent]; if( ! view ) view = item; [invocation setSelector:@selector( contextualMenuItemsForObject:inView: )]; [invocation setArgument:&item atIndex:2]; [invocation setArgument:&view atIndex:3]; NSArray *results = [[MVChatPluginManager defaultManager] makePluginsPerformInvocation:invocation]; if( [results count] ) { [menu addItem:[NSMenuItem separatorItem]]; NSArray *items = nil; enumerator = [results objectEnumerator]; while( ( items = [enumerator nextObject] ) ) { if( ! [items respondsToSelector:@selector( objectEnumerator )] ) continue; NSEnumerator *ienumerator = [items objectEnumerator]; while( ( menuItem = [ienumerator nextObject] ) ) if( [menuItem isKindOfClass:[NSMenuItem class]] ) [menu addItem:menuItem]; } if( [[[menu itemArray] lastObject] isSeparatorItem] ) [menu removeItem:[[menu itemArray] lastObject]]; } if( [menu numberOfItems] ) { [viewActionButton setEnabled:YES]; [viewActionButton setMenu:menu]; } else [viewActionButton setEnabled:NO]; } - (void) _refreshWindow { [[self window] disableFlushWindow]; id item = [self selectedListItem]; if( ( [item conformsToProtocol:@protocol( JVChatViewController )] && item != (id) _activeViewController ) || ( ! _activeViewController && [[item parent] conformsToProtocol:@protocol( JVChatViewController )] && ( item = [item parent] ) ) ) { id lastActive = _activeViewController; if( [_activeViewController respondsToSelector:@selector( willUnselect )] ) [(NSObject *)_activeViewController willUnselect]; if( [item respondsToSelector:@selector( willSelect )] ) [(NSObject *)item willSelect]; [_activeViewController autorelease]; _activeViewController = [item retain]; [[self window] setContentView:[_activeViewController view]]; [[self window] setToolbar:[_activeViewController toolbar]]; [[self window] makeFirstResponder:[[_activeViewController view] nextKeyView]]; if( [lastActive respondsToSelector:@selector( didUnselect )] ) [(NSObject *)lastActive didUnselect]; if( [_activeViewController respondsToSelector:@selector( didSelect )] ) [(NSObject *)_activeViewController didSelect]; } else if( ! [_views count] || ! _activeViewController ) { [[self window] setContentView:_placeHolder]; [[[self window] toolbar] setDelegate:nil]; [[self window] setToolbar:nil]; [[self window] makeFirstResponder:nil]; } [self _refreshWindowTitle]; [[self window] enableFlushWindow]; } - (void) _refreshWindowTitle { NSString *title = [_activeViewController windowTitle]; if( ! title ) title = @""; [[self window] setTitle:title]; } - (void) _refreshList { id selectItem = [self selectedListItem]; [chatViewsOutlineView reloadData]; [chatViewsOutlineView sizeLastColumnToFit]; if( selectItem ) { int selectedRow = [chatViewsOutlineView rowForItem:selectItem]; [chatViewsOutlineView selectRow:selectedRow byExtendingSelection:NO]; } } - (void) _switchViews:(id) sender { [self showChatViewController:[sender representedObject]]; } @end #pragma mark - @implementation JVChatWindowController (JVChatWindowControllerScripting) - (NSNumber *) uniqueIdentifier { return [NSNumber numberWithUnsignedInt:(unsigned long) self]; } #pragma mark - - (NSArray *) views { return _views; } - (id ) valueInViewsAtIndex:(unsigned) index { return [_views objectAtIndex:index]; } - (id ) valueInViewsWithUniqueID:(id) identifier { NSEnumerator *enumerator = [_views objectEnumerator]; id view = nil; while( ( view = [enumerator nextObject] ) ) if( [[view uniqueIdentifier] isEqual:identifier] ) return view; return nil; } - (id ) valueInViewsWithName:(NSString *) name { NSEnumerator *enumerator = [_views objectEnumerator]; id view = nil; while( ( view = [enumerator nextObject] ) ) if( [[view title] isEqualToString:name] ) return view; return nil; } - (void) addInViews:(id ) view { [self addChatViewController:view]; } - (void) insertInViews:(id ) view { [self addChatViewController:view]; } - (void) insertInViews:(id ) view atIndex:(int) index { [self insertChatViewController:view atIndex:index]; } - (void) removeFromViewsAtIndex:(unsigned) index { [self removeChatViewControllerAtIndex:index]; } - (void) replaceInViews:(id ) view atIndex:(unsigned) index { [self replaceChatViewControllerAtIndex:index withController:view]; } #pragma mark - - (NSArray *) chatViewsWithClass:(Class) class { NSMutableArray *ret = [NSMutableArray array]; NSEnumerator *enumerator = [_views objectEnumerator]; id item = nil; while( ( item = [enumerator nextObject] ) ) if( [item isMemberOfClass:class] ) [ret addObject:item]; return ret; } - (id ) valueInChatViewsAtIndex:(unsigned) index withClass:(Class) class { return [[self chatViewsWithClass:class] objectAtIndex:index]; } - (id ) valueInChatViewsWithUniqueID:(id) identifier andClass:(Class) class { return [self valueInViewsWithUniqueID:identifier]; } - (id ) valueInChatViewsWithName:(NSString *) name andClass:(Class) class { NSEnumerator *enumerator = [[self chatViewsWithClass:class] objectEnumerator]; id view = nil; while( ( view = [enumerator nextObject] ) ) if( [[view title] isEqualToString:name] ) return view; return nil; } - (void) addInChatViews:(id ) view withClass:(Class) class { unsigned int index = [_views indexOfObject:[[self chatViewsWithClass:class] lastObject]]; [self insertChatViewController:view atIndex:( index + 1 )]; } - (void) insertInChatViews:(id ) view atIndex:(unsigned) index withClass:(Class) class { if( index == [[self chatViewsWithClass:class] count] ) { [self addInChatViews:view withClass:class]; } else { unsigned int indx = [_views indexOfObject:[[self chatViewsWithClass:class] objectAtIndex:index]]; [self insertChatViewController:view atIndex:indx]; } } - (void) removeFromChatViewsAtIndex:(unsigned) index withClass:(Class) class { unsigned int indx = [_views indexOfObject:[[self chatViewsWithClass:class] objectAtIndex:index]]; [self removeChatViewControllerAtIndex:indx]; } - (void) replaceInChatViews:(id ) view atIndex:(unsigned) index withClass:(Class) class { unsigned int indx = [_views indexOfObject:[[self chatViewsWithClass:class] objectAtIndex:index]]; [self replaceChatViewControllerAtIndex:indx withController:view]; } #pragma mark - - (NSArray *) chatRooms { return [self chatViewsWithClass:[JVChatRoom class]]; } - (id ) valueInChatRoomsAtIndex:(unsigned) index { return [self valueInChatViewsAtIndex:index withClass:[JVChatRoom class]]; } - (id ) valueInChatRoomsWithUniqueID:(id) identifier { return [self valueInChatViewsWithUniqueID:identifier andClass:[JVChatRoom class]]; } - (id ) valueInChatRoomsWithName:(NSString *) name { return [self valueInChatViewsWithName:name andClass:[JVChatRoom class]]; } - (void) addInChatRooms:(id ) view { [self addInChatViews:view withClass:[JVChatRoom class]]; } - (void) insertInChatRooms:(id ) view { [self addInChatViews:view withClass:[JVChatRoom class]]; } - (void) insertInChatRooms:(id ) view atIndex:(unsigned) index { [self insertInChatViews:view atIndex:index withClass:[JVChatRoom class]]; } - (void) removeFromChatRoomsAtIndex:(unsigned) index { [self removeFromChatViewsAtIndex:index withClass:[JVChatRoom class]]; } - (void) replaceInChatRooms:(id ) view atIndex:(unsigned) index { [self replaceInChatViews:view atIndex:index withClass:[JVChatRoom class]]; } #pragma mark - - (NSArray *) directChats { return [self chatViewsWithClass:[JVDirectChat class]]; } - (id ) valueInDirectChatsAtIndex:(unsigned) index { return [self valueInChatViewsAtIndex:index withClass:[JVDirectChat class]]; } - (id ) valueInDirectChatsWithUniqueID:(id) identifier { return [self valueInChatViewsWithUniqueID:identifier andClass:[JVDirectChat class]]; } - (id ) valueInDirectChatsWithName:(NSString *) name { return [self valueInChatViewsWithName:name andClass:[JVDirectChat class]]; } - (void) addInDirectChats:(id ) view { [self addInChatViews:view withClass:[JVDirectChat class]]; } - (void) insertInDirectChats:(id ) view { [self addInChatViews:view withClass:[JVDirectChat class]]; } - (void) insertInDirectChats:(id ) view atIndex:(unsigned) index { [self insertInChatViews:view atIndex:index withClass:[JVDirectChat class]]; } - (void) removeFromDirectChatsAtIndex:(unsigned) index { [self removeFromChatViewsAtIndex:index withClass:[JVDirectChat class]]; } - (void) replaceInDirectChats:(id ) view atIndex:(unsigned) index { [self replaceInChatViews:view atIndex:index withClass:[JVDirectChat class]]; } #pragma mark - - (NSArray *) chatTranscripts { return [self chatViewsWithClass:[JVChatTranscript class]]; } - (id ) valueInChatTranscriptsAtIndex:(unsigned) index { return [self valueInChatViewsAtIndex:index withClass:[JVChatTranscript class]]; } - (id ) valueInChatTranscriptsWithUniqueID:(id) identifier { return [self valueInChatViewsWithUniqueID:identifier andClass:[JVChatTranscript class]]; } - (id ) valueInChatTranscriptsWithName:(NSString *) name { return [self valueInChatViewsWithName:name andClass:[JVChatTranscript class]]; } - (void) addInChatTranscripts:(id ) view { [self addInChatViews:view withClass:[JVChatTranscript class]]; } - (void) insertInChatTranscripts:(id ) view { [self addInChatViews:view withClass:[JVChatTranscript class]]; } - (void) insertInChatTranscripts:(id ) view atIndex:(unsigned) index { [self insertInChatViews:view atIndex:index withClass:[JVChatTranscript class]]; } - (void) removeFromChatTranscriptsAtIndex:(unsigned) index { [self removeFromChatViewsAtIndex:index withClass:[JVChatTranscript class]]; } - (void) replaceInChatTranscripts:(id ) view atIndex:(unsigned) index { [self replaceInChatViews:view atIndex:index withClass:[JVChatTranscript class]]; } #pragma mark - - (NSArray *) chatConsoles { return [self chatViewsWithClass:[JVChatConsole class]]; } - (id ) valueInChatConsolesAtIndex:(unsigned) index { return [self valueInChatViewsAtIndex:index withClass:[JVChatConsole class]]; } - (id ) valueInChatConsolesWithUniqueID:(id) identifier { return [self valueInChatViewsWithUniqueID:identifier andClass:[JVChatConsole class]]; } - (id ) valueInChatConsolesWithName:(NSString *) name { return [self valueInChatViewsWithName:name andClass:[JVChatConsole class]]; } - (void) addInChatConsoles:(id ) view { [self addInChatViews:view withClass:[JVChatConsole class]]; } - (void) insertInChatConsoles:(id ) view { [self addInChatViews:view withClass:[JVChatConsole class]]; } - (void) insertInChatConsoles:(id ) view atIndex:(unsigned) index { [self insertInChatViews:view atIndex:index withClass:[JVChatConsole class]]; } - (void) removeFromChatConsolesAtIndex:(unsigned) index { [self removeFromChatViewsAtIndex:index withClass:[JVChatConsole class]]; } - (void) replaceInChatConsoles:(id ) view atIndex:(unsigned) index { [self replaceInChatViews:view atIndex:index withClass:[JVChatConsole class]]; } #pragma mark - - (NSArray *) indicesOfObjectsByEvaluatingRangeSpecifier:(NSRangeSpecifier *) specifier { NSString *key = [specifier key]; if( [key isEqual:@"views"] || [key isEqual:@"chatRooms"] || [key isEqual:@"directChats"] || [key isEqual:@"chatConsoles"] || [key isEqual:@"chatTranscripts"] ) { NSScriptObjectSpecifier *startSpec = [specifier startSpecifier]; NSScriptObjectSpecifier *endSpec = [specifier endSpecifier]; NSString *startKey = [startSpec key]; NSString *endKey = [endSpec key]; NSArray *chatViews = [self views]; if( ! startSpec && ! endSpec ) return nil; if( ! [chatViews count] ) [NSArray array]; if( ( ! startSpec || [startKey isEqual:@"views"] || [startKey isEqual:@"chatRooms"] || [startKey isEqual:@"directChats"] || [startKey isEqual:@"chatConsoles"] || [startKey isEqual:@"chatTranscripts"] ) && ( ! endSpec || [endKey isEqual:@"views"] || [endKey isEqual:@"chatRooms"] || [endKey isEqual:@"directChats"] || [endKey isEqual:@"chatConsoles"] || [endKey isEqual:@"chatTranscripts"] ) ) { int startIndex = 0; int endIndex = 0; // The strategy here is going to be to find the index of the start and stop object in the full graphics array, regardless of what its key is. Then we can find what we're looking for in that range of the graphics key (weeding out objects we don't want, if necessary). // First find the index of the first start object in the graphics array if( startSpec ) { id startObject = [startSpec objectsByEvaluatingSpecifier]; if( [startObject isKindOfClass:[NSArray class]] ) { if( ! [(NSArray *)startObject count] ) startObject = nil; else startObject = [startObject objectAtIndex:0]; } if( ! startObject ) return nil; startIndex = [chatViews indexOfObjectIdenticalTo:startObject]; if( startIndex == NSNotFound ) return nil; } // Now find the index of the last end object in the graphics array if( endSpec ) { id endObject = [endSpec objectsByEvaluatingSpecifier]; if( [endObject isKindOfClass:[NSArray class]] ) { if( ! [(NSArray *)endObject count] ) endObject = nil; else endObject = [endObject lastObject]; } if( ! endObject ) return nil; endIndex = [chatViews indexOfObjectIdenticalTo:endObject]; if( endIndex == NSNotFound ) return nil; } else endIndex = ( [chatViews count] - 1 ); // Accept backwards ranges gracefully if( endIndex < startIndex ) { int temp = endIndex; endIndex = startIndex; startIndex = temp; } // Now startIndex and endIndex specify the end points of the range we want within the main array. // We will traverse the range and pick the objects we want. // We do this by getting each object and seeing if it actually appears in the real key that we are trying to evaluate in. NSMutableArray *result = [NSMutableArray array]; BOOL keyIsGeneric = [key isEqualToString:@"views"]; NSArray *rangeKeyObjects = ( keyIsGeneric ? nil : [self valueForKey:key] ); unsigned curKeyIndex = 0, i = 0; id obj = nil; for( i = startIndex; i <= endIndex; i++ ) { if( keyIsGeneric ) { [result addObject:[NSNumber numberWithInt:i]]; } else { obj = [chatViews objectAtIndex:i]; curKeyIndex = [rangeKeyObjects indexOfObjectIdenticalTo:obj]; if( curKeyIndex != NSNotFound ) [result addObject:[NSNumber numberWithInt:curKeyIndex]]; } } return result; } } return nil; } - (NSArray *) indicesOfObjectsByEvaluatingRelativeSpecifier:(NSRelativeSpecifier *) specifier { NSString *key = [specifier key]; if( [key isEqual:@"views"] || [key isEqual:@"chatRooms"] || [key isEqual:@"directChats"] || [key isEqual:@"chatConsoles"] || [key isEqual:@"chatTranscripts"] ) { NSScriptObjectSpecifier *baseSpec = [specifier baseSpecifier]; NSString *baseKey = [baseSpec key]; NSArray *chatViews = [self views]; NSRelativePosition relPos = [specifier relativePosition]; if( ! baseSpec ) return nil; if( ! [chatViews count] ) return [NSArray array]; if( [baseKey isEqual:@"views"] || [baseKey isEqual:@"chatRooms"] || [baseKey isEqual:@"directChats"] || [baseKey isEqual:@"chatConsoles"] || [baseKey isEqual:@"chatTranscripts"] ) { int baseIndex = 0; // The strategy here is going to be to find the index of the base object in the full graphics array, regardless of what its key is. Then we can find what we're looking for before or after it. // First find the index of the first or last base object in the master array // Base specifiers are to be evaluated within the same container as the relative specifier they are the base of. That's this container. id baseObject = [baseSpec objectsByEvaluatingWithContainers:self]; if( [baseObject isKindOfClass:[NSArray class]] ) { int baseCount = [(NSArray *)baseObject count]; if( baseCount ) { if( relPos == NSRelativeBefore ) baseObject = [baseObject objectAtIndex:0]; else baseObject = [baseObject objectAtIndex:( baseCount - 1 )]; } else baseObject = nil; } if( ! baseObject ) return nil; baseIndex = [chatViews indexOfObjectIdenticalTo:baseObject]; if( baseIndex == NSNotFound ) return nil; // Now baseIndex specifies the base object for the relative spec in the master array. // We will start either right before or right after and look for an object that matches the type we want. // We do this by getting each object and seeing if it actually appears in the real key that we are trying to evaluate in. NSMutableArray *result = [NSMutableArray array]; BOOL keyIsGeneric = [key isEqual:@"views"]; NSArray *relKeyObjects = ( keyIsGeneric ? nil : [self valueForKey:key] ); unsigned curKeyIndex = 0, viewCount = [chatViews count]; id obj = nil; if( relPos == NSRelativeBefore ) baseIndex--; else baseIndex++; while( baseIndex >= 0 && baseIndex < viewCount ) { if( keyIsGeneric ) { [result addObject:[NSNumber numberWithInt:baseIndex]]; break; } else { obj = [chatViews objectAtIndex:baseIndex]; curKeyIndex = [relKeyObjects indexOfObjectIdenticalTo:obj]; if( curKeyIndex != NSNotFound ) { [result addObject:[NSNumber numberWithInt:curKeyIndex]]; break; } } if( relPos == NSRelativeBefore ) baseIndex--; else baseIndex++; } return result; } } return nil; } - (NSArray *) indicesOfObjectsByEvaluatingObjectSpecifier:(NSScriptObjectSpecifier *) specifier { if( [specifier isKindOfClass:[NSRangeSpecifier class]] ) { return [self indicesOfObjectsByEvaluatingRangeSpecifier:(NSRangeSpecifier *) specifier]; } else if( [specifier isKindOfClass:[NSRelativeSpecifier class]] ) { return [self indicesOfObjectsByEvaluatingRelativeSpecifier:(NSRelativeSpecifier *) specifier]; } return nil; } @end