Prevent dragging groups into groups. Fixes #8706
[adiumx.git] / Source / AIXtrasManager.m
blob4ff270853db8c49c76dfc6a6b99cd757274a45cf
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  * 
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
17 #import "AIXtrasManager.h"
18 #import "AIXtraInfo.h"
19 #import "AIXtraPreviewController.h"
20 #import <Adium/AIDockControllerProtocol.h>
21 #import <Adium/AIPathUtilities.h>
22 #import <Adium/KNShelfSplitView.h>
23 #import <AIUtilities/AIArrayAdditions.h>
24 #import <AIUtilities/AIAlternatingRowTableView.h>
25 #import <AIUtilities/AIFileManagerAdditions.h>
26 #import <AIUtilities/AIImageTextCell.h>
27 #import <AIUtilities/AIImageAdditions.h>
28 #import <AIUtilities/AIToolbarUtilities.h>
30 #define ADIUM_XTRAS_PAGE                AILocalizedString(@"http://www.adiumxtras.com/","Adium xtras page. Localized only if a translated version exists.")
31 #define DELETE                                  AILocalizedStringFromTable(@"Delete", @"Buttons", nil)
32 #define GET_MORE_XTRAS                  AILocalizedStringFromTable(@"Get More Xtras", @"Buttons", "Button in the Xtras Manager to go to adiumxtras.com to get more adiumxtras")
34 #define MINIMUM_SOURCE_LIST_WIDTH 40
36 @interface AIXtrasManager (PRIVATE)
37 - (void)installToolbar;
38 - (void)updateForSelectedCategory;
39 @end
41 @implementation AIXtrasManager
43 static AIXtrasManager *manager;
45 + (AIXtrasManager *) sharedManager
47         return manager;
50 - (void)installPlugin
52         manager = self;
55 - (void)windowDidLoad
57         [window setTitle:AILocalizedString(@"Xtras Manager", "Xtras Manager window title")];
59         [self installToolbar];
61         //This is the Mail.app source list background color... which differs from the iTunes one.
62         [tableView_categories setBackgroundColor:[NSColor colorWithCalibratedRed:.9059
63                                                                                                                                            green:.9294
64                                                                                                                                                 blue:.9647
65                                                                                                                                            alpha:1.0]];
66         [tableView_categories setDrawsGradientSelection:YES];
67         
68         AIImageTextCell                 *cell;
69         //Configure our tableViews
70         cell = [[AIImageTextCell alloc] init];
71         [cell setFont:[NSFont systemFontOfSize:12]];
72         [[tableView_categories tableColumnWithIdentifier:@"name"] setDataCell:cell];
73         [cell release];
74         
75         cell = [[AIImageTextCell alloc] init];
76         [cell setFont:[NSFont systemFontOfSize:12]];
77         [[xtraList tableColumnWithIdentifier:@"xtras"] setDataCell:cell];
78         [cell release];
80         //XXX ???
81         [previewContainerView setHasVerticalScroller:YES];
82         [previewContainerView setAutohidesScrollers:YES];
83         [previewContainerView setBorderType:NSBezelBorder];
85         [tableView_categories selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO];
88 - (void)showXtras
90         if (!window) {
91                 [self loadXtras];
92                 
93                 showInfo = NO;
94                 
95                 [[adium notificationCenter] addObserver:self
96                                                                            selector:@selector(xtrasChanged:)
97                                                                                    name:AIXtrasDidChangeNotification
98                                                                                  object:nil];
99                 [NSBundle loadNibNamed:@"XtrasManager" owner:self];
100                 [self windowDidLoad];
101         }
102                 
103         [window makeKeyAndOrderFront:nil];
106 - (void)windowWillClose:(NSNotification *)aNotification
108         [[adium notificationCenter] removeObserver:self
109                                                                                   name:AIXtrasDidChangeNotification
110                                                                                 object:nil];
111         
112         [categories release]; categories = nil;
113         [toolbarItems release]; toolbarItems = nil;
115         //Release top-level nib objects besides the window
116         [view_content release]; view_content = nil;
117         [view_shelf release]; view_shelf = nil; 
119         //XXX don't need to do this when this is a window controller
120         window = nil;
124 - (void)xtrasChanged:(NSNotification *)not
126         //Clear our cache of loaded Xtras
127         [self loadXtras];
128         
129         //Now redisplay our current category, in case it changed
130         [self updateForSelectedCategory];
133 int categorySort(id categoryA, id categoryB, void * context)
135         return [[categoryA objectForKey:@"Name"] caseInsensitiveCompare:[categoryB objectForKey:@"Name"]];
138 - (void)loadXtras
140         [categories release];
141         categories = [[NSMutableArray alloc] init];
142         
143         [categories addObject:[NSDictionary dictionaryWithObjectsAndKeys:
144                 [NSNumber numberWithInt:AIMessageStylesDirectory], @"Directory",
145                 AILocalizedString(@"Message Styles", "AdiumXtras category name"), @"Name",
146                 [NSImage imageNamed:@"AdiumMessageStyle"], @"Image", nil]];
148         [categories addObject:[NSDictionary dictionaryWithObjectsAndKeys:
149                 [NSNumber numberWithInt:AIContactListDirectory], @"Directory",
150                 AILocalizedString(@"Contact List Themes", "AdiumXtras category name"), @"Name",
151                 [NSImage imageNamed:@"AdiumListTheme"], @"Image", nil]];
152         
154         [categories addObject:[NSDictionary dictionaryWithObjectsAndKeys:
155                 [NSNumber numberWithInt:AIStatusIconsDirectory], @"Directory",
156                 AILocalizedString(@"Status Icons", "AdiumXtras category name"), @"Name",
157                 [NSImage imageNamed:@"AdiumStatusIcons"], @"Image", nil]];
158         
159         [categories addObject:[NSDictionary dictionaryWithObjectsAndKeys:
160                 [NSNumber numberWithInt:AISoundsDirectory], @"Directory",
161                 AILocalizedString(@"Sound Sets", "AdiumXtras category name"), @"Name",
162                 [NSImage imageNamed:@"AdiumSoundset"], @"Image", nil]];
163         
164         [categories addObject:[NSDictionary dictionaryWithObjectsAndKeys:
165                 [NSNumber numberWithInt:AIDockIconsDirectory], @"Directory",
166                 AILocalizedString(@"Dock Icons", "AdiumXtras category name"), @"Name",
167                 [NSImage imageNamed:@"AdiumIcon"], @"Image", nil]];
168         
169         [categories addObject:[NSDictionary dictionaryWithObjectsAndKeys:
170                 [NSNumber numberWithInt:AIEmoticonsDirectory], @"Directory",
171                 AILocalizedString(@"Emoticons", "AdiumXtras category name"), @"Name",
172                 [NSImage imageNamed:@"AdiumEmoticonset"], @"Image", nil]];
173         
174         [categories addObject:[NSDictionary dictionaryWithObjectsAndKeys:
175                 [NSNumber numberWithInt:AIScriptsDirectory], @"Directory",
176                 AILocalizedString(@"Scripts", "AdiumXtras category name"), @"Name",
177                 [NSImage imageNamed:@"AdiumScripts"], @"Image", nil]];
179         [categories addObject:[NSDictionary dictionaryWithObjectsAndKeys:
180                 [NSNumber numberWithInt:AIServiceIconsDirectory], @"Directory",
181                 AILocalizedString(@"Service Icons", "AdiumXtras category name"), @"Name",
182                 [NSImage imageNamed:@"AdiumServiceIcons"], @"Image", nil]];
183         
184         [categories addObject:[NSDictionary dictionaryWithObjectsAndKeys:
185                 [NSNumber numberWithInt:AIMenuBarIconsDirectory], @"Directory",
186                 AILocalizedString(@"Menu Bar Icons", "AdiumXtras category name"), @"Name",
187                 [NSImage imageNamed:@"AdiumMenuBarIcons"], @"Image", nil]];
188         
189         [categories sortUsingFunction:categorySort context:NULL];
192 - (NSArray *)arrayOfXtrasAtPaths:(NSArray *)paths
194         NSMutableArray  *contents = [NSMutableArray array];
195         NSEnumerator    *dirEnu;
196         NSString                *path, *xtraName;
197         NSFileManager   *manager = [NSFileManager defaultManager];
199         dirEnu = [paths objectEnumerator];
200         while ((path = [dirEnu nextObject])) {
201                 NSEnumerator    *xEnu;
203                 xEnu = [[manager directoryContentsAtPath:path] objectEnumerator];
204                 while ((xtraName = [xEnu nextObject])) {
205                         if (![xtraName hasPrefix:@"."]) {
206                                 [contents addObject:[AIXtraInfo infoWithURL:[NSURL fileURLWithPath:[path stringByAppendingPathComponent:xtraName]]]];
207                         }
208                 }
209         }
211         return contents;
214 - (void)dealloc
216         [categories release];
218         [super dealloc];
221 - (NSArray *)xtrasForCategoryAtIndex:(int)inIndex
223         if (inIndex == -1) return nil;
225         NSDictionary    *xtrasDict = [categories objectAtIndex:inIndex];
226         NSArray                 *xtras;
227         
228         if (!(xtras = [xtrasDict objectForKey:@"Xtras"])) {
229                 xtras = [self arrayOfXtrasAtPaths:AISearchPathForDirectoriesInDomains([[xtrasDict objectForKey:@"Directory"] intValue],
230                                                                                                                                                           AIAllDomainsMask & ~AIInternalDomainMask,
231                                                                                                                                                           YES)];
232                 NSMutableDictionary *newDictionary = [xtrasDict mutableCopy];
233                 [newDictionary setObject:xtras forKey:@"Xtras"];
234                 [categories replaceObjectAtIndex:inIndex
235                                                           withObject:newDictionary];
236                 [newDictionary release];
237         }
238         
239         return xtras;
242 - (void)updateForSelectedCategory
244         [selectedCategory autorelease];
245         selectedCategory = [[self xtrasForCategoryAtIndex:[tableView_categories selectedRow]] retain];
247         [xtraList reloadData];
248         if ([xtraList numberOfRows]) {
249                 [xtraList selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO];
250         }
252         [self updatePreview];
255 - (void)updatePreview
257         AIXtraInfo *xtra = nil;
259         if ([selectedCategory count] > 0 && [xtraList selectedRow] != -1) {
260                 xtra = [selectedCategory objectAtIndex:[xtraList selectedRow]];
261         }
263         if (xtra) {
264                 //[showInfoControl setHidden:NO];
265                 if(showInfo)
266                         [NSBundle loadNibNamed:@"XtraInfoView" owner:self];
267                 else {
268                         [NSBundle loadNibNamed:@"XtraPreviewImageView" owner:self];
269                         /*      NSString * xtraType = [xtra type];
270                         
271                         if ([xtraType isEqualToString:AIXtraTypeEmoticons])
272                         [NSBundle loadNibNamed:@"EmoticonPreviewView" owner:self];
273                         else if ([xtraType isEqualToString:AIXtraTypeDockIcon])
274                         [NSBundle loadNibNamed:@"DockIconPreviewView" owner:self];
275                         else if ([xtraType isEqualToString:AIXtraTypeMessageStyle])
276                         [NSBundle loadNibNamed:@"WebkitMessageStylePreviewView" owner:self];
277                         else if ([xtraType isEqualToString:AIXtraTypeStatusIcons]) {
278                                 [NSBundle loadNibNamed:@"StatusIconPreviewView" owner:self];
279                         }
280                         else if ([xtraType isEqualToString:AIXtraTypeServiceIcons]) {
281                                 [NSBundle loadNibNamed:@"ServiceIconPreviewView" owner:self];
282                         }
283                         else { //catchall behavior is to just show the readme
284                                 [NSBundle loadNibNamed:@"XtraInfoView" owner:self];
285                                 [showInfoControl setHidden:YES];
286                         }*/
287                 }
288                 if (previewController/* && previewContainerView*/) {
289                         NSView *pv = [previewController previewView];
290                         NSSize docSize = [previewContainerView documentVisibleRect].size;
291                         NSRect viewFrame = [pv frame];
292                         viewFrame.size.width = docSize.width;
293                         if([pv respondsToSelector:@selector(image)]) viewFrame.size.height = [[(NSImageView *)pv image]size].height;
294                         if(viewFrame.size.height < docSize.height) viewFrame.size.height = docSize.height;
295                         [pv setFrameSize:viewFrame.size];
296                         [previewContainerView setDocumentView:pv];
297                         [previewController setXtra:xtra];
298                         [previewContainerView setNeedsDisplay:YES];
299                 }               
300         }
303 - (IBAction) setShowsInfo:(id)sender
305         showInfo = ([sender selectedSegment] != 0);
307         [self updatePreview];
310 - (void)deleteXtrasAlertDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo
312         if (returnCode == NSAlertDefaultReturn) {
313                 NSFileManager * manager = [NSFileManager defaultManager];
314                 NSIndexSet * indices = [xtraList selectedRowIndexes];
315                 NSMutableSet * pathExtensions = [NSMutableSet set];
316                 NSString * path;
317                 for (int i = [indices lastIndex]; i >= 0; i--) {
318                         if ([indices containsIndex:i]) {
319                                 path = [[selectedCategory objectAtIndex:i] path];
320                                 [pathExtensions addObject:[path pathExtension]];
321                                 [manager trashFileAtPath:path];
322                         }
323                 }
324                 [xtraList selectRow:0 byExtendingSelection:NO];
325                 [selectedCategory removeObjectsAtIndexes:indices];
326                 [xtraList reloadData];
327                 /*
328                  XXX this is ugly. We should use the AIXtraInfo's type instead of the path extension
329                 */
330                 NSEnumerator * extEnu = [pathExtensions objectEnumerator];
331                 while ((path = [extEnu nextObject])) { //usually this will only run once
332                         [[adium notificationCenter] postNotificationName:AIXtrasDidChangeNotification
333                                                                                                           object:path];
334                 }
335         }
338 - (IBAction) deleteXtra:(id)sender
340         int selectionCount = [[xtraList selectedRowIndexes] count];
342         NSAlert * warning = [NSAlert alertWithMessageText:((selectionCount > 1) ?
343                                                                                                            [NSString stringWithFormat:AILocalizedString(@"Delete %i Xtras?", nil), selectionCount] :
344                                                                                                            AILocalizedString(@"Delete Xtra?", nil))
345                                                                                 defaultButton:AILocalizedString(@"Delete", nil)
346                                                                           alternateButton:AILocalizedString(@"Cancel", nil)
347                                                                                   otherButton:nil
348                                                         informativeTextWithFormat:((selectionCount > 1) ?
349                                                                                                            AILocalizedString(@"The selected Xtras will be moved to the Trash.", nil) :
350                                                                                                            AILocalizedString(@"The selected Xtra will be moved to the Trash.", nil))];
351         [warning beginSheetModalForWindow:window
352                                                 modalDelegate:self
353                                            didEndSelector:@selector(deleteXtrasAlertDidEnd:returnCode:contextInfo:)
354                                                   contextInfo:nil];
357 - (IBAction)browseXtras:(id)sender
359         [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:ADIUM_XTRAS_PAGE]];
362 - (IBAction)checkForUpdates:(id)sender
364         
367 + (BOOL)createXtraBundleAtPath:(NSString *)path 
369         NSString *contentsPath  = [path stringByAppendingPathComponent:@"Contents"];
370         NSString *resourcesPath = [contentsPath stringByAppendingPathComponent:@"Resources"];
371         NSString *infoPlistPath = [contentsPath stringByAppendingPathComponent:@"Info.plist"];
373         NSFileManager * manager = [NSFileManager defaultManager];
374         NSString * name = [[path lastPathComponent] stringByDeletingPathExtension];
375         if (![manager fileExistsAtPath:path]) {
376                 [manager createDirectoryAtPath:path attributes:nil];
377                 [manager createDirectoryAtPath:contentsPath attributes:nil];
379                 //Info.plist
380                 [[NSDictionary dictionaryWithObjectsAndKeys:
381                         @"English", kCFBundleDevelopmentRegionKey,
382                         name, kCFBundleNameKey,
383                         @"AdIM", @"CFBundlePackageType",
384                         [@"com.adiumx." stringByAppendingString:name], kCFBundleIdentifierKey,
385                         [NSNumber numberWithInt:1], @"XtraBundleVersion",
386                         @"1.0", kCFBundleInfoDictionaryVersionKey,
387                         nil] writeToFile:infoPlistPath atomically:YES];
389                 //Resources
390                 [manager createDirectoryAtPath:resourcesPath attributes:nil];
391         }
393         BOOL isDir = NO, success;
394         success = [manager fileExistsAtPath:resourcesPath isDirectory:&isDir] && isDir;
395         if (success)
396                 success = [manager fileExistsAtPath:infoPlistPath isDirectory:&isDir] && !isDir;
397         return success;
400 - (void)tableView:(NSTableView *)tableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn row:(int)row
402         if (tableView == tableView_categories) {
403                 [cell setImage:[[categories objectAtIndex:row] objectForKey:@"Image"]];
404                 [cell setSubString:nil];
405         }
406         else {
407                 [cell setImage:[[selectedCategory objectAtIndex:row] icon]];
408                 [cell setSubString:nil];
409         }
412 - (int)numberOfRowsInTableView:(NSTableView *)tableView
414         if (tableView == tableView_categories) {
415                 return [categories count];
416         }
417         else {
418                 return [selectedCategory count];
419         }
422 - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(int)row
424         if (tableView == tableView_categories) {
425                 return [[categories objectAtIndex:row] objectForKey:@"Name"];
426         } else {
427                 NSString * name = [[selectedCategory objectAtIndex:row] name];
428                 return (name != nil) ? name : @"";
429         }
432 - (void)tableViewSelectionDidChange:(NSNotification *)aNotification
434         if ([aNotification object] == xtraList) {
435                 //int   selectedRow = [xtraList selectedRow];
436                 //if ((selectedRow >= 0) && (selectedRow < [selectedCategory count])) {
437                         //AIXtraInfo *xtraInfo  = [AIXtraInfo infoWithURL:[NSURL fileURLWithPath:[[selectedCategory objectAtIndex:selectedRow] path]]];
438                 //      if ([[xtraList selectedRowIndexes] count] == 1)
439                 //              [previewController setXtra:xtraInfo];
440                 //}
441                 
442         } else if ([aNotification object] == tableView_categories) {
443                 [self updateForSelectedCategory];
444         }
447 - (void)tableViewDeleteSelectedRows:(NSTableView *)tableView
449         [self deleteXtra:tableView];
452 #pragma mark Placeholder until this is a window controller
453 - (NSWindow *)window
455         return window;
458 #pragma mark Toolbar
460 - (void)installToolbar
461 {       
462     NSToolbar           *toolbar = [[[NSToolbar alloc] initWithIdentifier:@"XtrasManager:Toolbar"] autorelease];
463         
464     [toolbar setDelegate:self];
465     [toolbar setDisplayMode:NSToolbarDisplayModeIconAndLabel];
466     [toolbar setSizeMode:NSToolbarSizeModeRegular];
467     [toolbar setVisible:YES];
468     [toolbar setAllowsUserCustomization:YES];
469     [toolbar setAutosavesConfiguration:YES];
470     toolbarItems = [[NSMutableDictionary alloc] init];
471         
472         //Delete Logs
473         [AIToolbarUtilities addToolbarItemToDictionary:toolbarItems
474                                                                         withIdentifier:@"delete"
475                                                                                          label:DELETE
476                                                                           paletteLabel:DELETE
477                                                                                    toolTip:AILocalizedString(@"Delete the selection",nil)
478                                                                                         target:self
479                                                                    settingSelector:@selector(setImage:)
480                                                                            itemContent:[NSImage imageNamed:@"remove" forClass:[self class]]
481                                                                                         action:@selector(deleteXtra:)
482                                                                                           menu:nil];
484         [AIToolbarUtilities addToolbarItemToDictionary:toolbarItems
485                                                                         withIdentifier:@"getmoreXtras"
486                                                                                          label:GET_MORE_XTRAS
487                                                                           paletteLabel:GET_MORE_XTRAS
488                                                                                    toolTip:GET_MORE_XTRAS
489                                                                                         target:self
490                                                                    settingSelector:@selector(setImage:)
491                                                                            itemContent:[NSImage imageNamed:@"xtras_duck" forClass:[self class]]
492                                                                                         action:@selector(browseXtras:)
493                                                                                           menu:nil];
494         
495         [[self window] setToolbar:toolbar];
496 }       
498 - (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag
500     return [AIToolbarUtilities toolbarItemFromDictionary:toolbarItems withIdentifier:itemIdentifier];
503 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar*)toolbar
505     return [NSArray arrayWithObjects:@"getmoreXtras", NSToolbarFlexibleSpaceItemIdentifier, @"delete", nil];
508 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar*)toolbar
510     return [[toolbarItems allKeys] arrayByAddingObjectsFromArray:
511                 [NSArray arrayWithObjects:NSToolbarSeparatorItemIdentifier,
512                         NSToolbarSpaceItemIdentifier,
513                         NSToolbarFlexibleSpaceItemIdentifier,
514                         NSToolbarCustomizeToolbarItemIdentifier, nil]];
517 - (BOOL)validateToolbarItem:(NSToolbarItem *)theItem
519         if ([[theItem itemIdentifier] isEqualToString:@"delete"]) {
520                 return ([[xtraList selectedRowIndexes] count] > 0);
522         } else {
523                 return YES;
524         }
527 - (float)shelfSplitView:(KNShelfSplitView *)shelfSplitView validateWidth:(float)proposedWidth
529         return ((proposedWidth > MINIMUM_SOURCE_LIST_WIDTH) ? proposedWidth : MINIMUM_SOURCE_LIST_WIDTH);
532 @end