Added the Add Bookmark toolbar item to the default toolbar. Refs #8772
[adiumx.git] / Source / AISCLViewPlugin.m
blobf1c54b353e6fdbc63eae422d3da0a31931dc6623
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  */
16 #import "AISCLViewPlugin.h"
17 #import "ESContactListAdvancedPreferences.h"
18 #import "AIBorderlessListWindowController.h"
19 #import "AIStandardListWindowController.h"
20 #import "AIListOutlineView.h"
21 #import <Adium/AIInterfaceControllerProtocol.h>
22 #import <Adium/AIPreferenceControllerProtocol.h>
23 #import <Adium/AIMenuControllerProtocol.h>
24 #import <Adium/AIListGroup.h>
25 #import <AIUtilities/AIDictionaryAdditions.h>
26 #import <AIUtilities/AIMenuAdditions.h>
27 #import <AIUtilities/AIStringAdditions.h>
29 #define PREF_GROUP_APPEARANCE           @"Appearance"
31 #define DETACHED_DEFAULT_WINDOW                 @"Default Window"
32 #define DETACHED_WINDOWS                                @"Windows"
33 #define DETACHED_WINDOW_GROUPS                  @"Groups"
34 #define DETACHED_WINDOW_LOCATION                @"Location"
36 @interface AISCLViewPlugin (PRIVATE)
37 - (void)loadDetachedGroups;
38 - (void)loadWindowPreferences:(NSDictionary *)windowPreferences;
39 - (void)saveAndCloseDetachedGroups;
41 - (BOOL)rebuildContextMenu;
42 - (NSString *)formatContextMenu:(AIListObject<AIContainingObject> *)contactList;
43 - (NSString *)formatContextMenu:(AIListObject<AIContainingObject> *)contactList showEmpty:(BOOL)empty;
44 @end
46 /*!
47  * @class AISCLViewPlugin
48  * @brief This component plugin is responsible for controlling the main contact list and detached contact lists window and view.
49  *
50  * Either an AIStandardListWindowController or AIBorderlessListWindowController, each of which is a subclass of AIListWindowController,
51  * is instantiated. This window controller, with the help of the plugin, will be responsible for display of an AIListOutlineView.
52  * The borderless window controller uses an AIBorderlessListOutlineView.
53  *
54  * In either case, the outline view itself is controlled by an instance of AIListController.
55  *
56  * AISCLViewPlugin's class methods also manage ListLayout and ListTheme preference sets. ListLayout sets determine the contents and layout
57  * of the contact list; ListTheme sets control the colors used in the contact list.
58  */
59 @implementation AISCLViewPlugin
61 - (void)installPlugin
63         // List of windows
64         contactLists = [[NSMutableArray alloc] init];
65         
66     [[adium interfaceController] registerContactListController:self];
67         
68         //Install our preference view
69         advancedPreferences = [[ESContactListAdvancedPreferences preferencePane] retain];
71         //Context submenu
72         contextSubmenu = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Attach / Detach", "Menu item for attaching and detatching groups")
73                                                                                                 target:self
74                                                                                                 action:@selector(detachOrAttachMenuAction:)
75                                                                                  keyEquivalent:@""];
76         [[adium menuController] addContextualMenuItem:contextSubmenu toLocation:Context_Group_Manage];
78         //Control detached groups menu
79         [[adium menuController] addMenuItem:[NSMenuItem separatorItem] toLocation:LOC_Window_Commands];
80         menuItem_consolidate = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Consolidate Detached Groups", "menu item title")
81                                                                                                           target:self
82                                                                                                           action:@selector(closeDetachedContactLists) 
83                                                                                            keyEquivalent:@""];
84         [[adium menuController] addMenuItem:menuItem_consolidate toLocation:LOC_Window_Commands];
85         menuItem_nextDetached = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Next Detached Group", "menu item title")
86                                                                                                            target:self
87                                                                                                            action:@selector(nextDetachedContactList) 
88                                                                                                 keyEquivalent:@""];
89         [[adium menuController] addMenuItem:menuItem_nextDetached toLocation:LOC_Window_Commands];
90         menuItem_previousDetached = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Previous Detached Group", "menu item title")
91                                                                                                                    target:self
92                                                                                                                    action:@selector(previousDetachedContactList) 
93                                                                                                         keyEquivalent:@""];
94         [[adium menuController] addMenuItem:menuItem_previousDetached toLocation:LOC_Window_Commands];
95         
96         
97         //Observe list closing
98         [[adium notificationCenter] addObserver:self
99                                                                    selector:@selector(contactListDidClose:)
100                                                                            name:Interface_ContactListDidClose
101                                                                          object:nil];
102         
103         [[adium notificationCenter] addObserver:self
104                                                                    selector:@selector(contactListIsEmpty:)
105                                                                            name:DetachedContactListIsEmpty
106                                                                          object:nil];
107         
108         //Now register our other defaults
109     [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:CONTACT_LIST_DEFAULTS
110                                                                                                                                                 forClass:[self class]]
111                                                                                   forGroup:PREF_GROUP_CONTACT_LIST];                                                                              
112                                                                                           
113         //Observe window style changes 
114         [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_APPEARANCE];
116         //Detached state
117         hasLoaded = NO;
118         detachedCycle = 0;
119         detachable = YES;
122 - (void)uninstallPlugin
124         [contextSubmenu release];
125         [contextMenuDetach release];
126         [contextMenuAttach release];
127         [menuItem_allowDetach release];
128         [menuItem_previousDetached release];
129         [menuItem_nextDetached release];
130         [menuItem_consolidate release];
131         
132         [[adium notificationCenter] removeObserver:self];
133         [[adium preferenceController] unregisterPreferenceObserver:self];
136 //Contact List Windows -------------------------------------------------------------------------------------------------
137 #pragma mark Contact List Window
140  * @brief Creates a new window with a specified contact list
142  * @param contactList contaclist to be used in new contact list window
144  * @return Newly created contact list window controller
145  */
146 - (id)detachContactList:(AIListGroup *)contactList 
148         if ([contactList isKindOfClass:[AIListGroup class]]) { 
149                 AIListWindowController  *newContactList = [AIBorderlessListWindowController listWindowControllerForContactList:contactList];
150         
151                 [contactLists addObject:[newContactList retain]];
152                 [newContactList showWindowInFrontIfAllowed:YES];
153                 
154                 return newContactList;
155         }
156         
157         return nil;
161  * @brief Closes window specified 
163  * @param windowController Controller of window that will be closed (although 
164  * this could be used with any contact list window controller, it should only
165  * be used with detached contact lists)
166  */
167 - (void)closeContactList:(AIListWindowController *)window
168 {               
169         // Close contact list   
170         [[window window] performClose:nil];
174  * @brief Closes contact list based on given AIListOutlineView or AIListObject
176  * @param notification Notification containing either an AIListOutlineView or 
177  * AIListObject object to be used to determin contact list's window. 
178  */
179 - (void)contactListIsEmpty:(NSNotification *)notification
181         NSEnumerator *i = [contactLists objectEnumerator];
182         id object = [notification object];
183         AIListWindowController *window;
184         
185         while ((window = [i nextObject])) {
186                 if (([object isKindOfClass:[AIListOutlineView class]] && [window contactListView] == object)
187                         ||([object isKindOfClass:[AIListObject class]] && [[window listController] contactList] == object)){
188                         [self closeContactList:window];
189                         return;
190                 }
191         }
194 //Contact List Controller ----------------------------------------------------------------------------------------------
195 #pragma mark Contact List Controller
198  * @brief Retrieve the AIListWindowController in use
199  */
200 - (AIListWindowController *)contactListWindowController {
201         return defaultController;
205  * @brief Brings main contact list to either front or back
207  * @param bringToFront Wether to bring contact list to front of back
208  */
209 - (void)showContactListAndBringToFront:(BOOL)bringToFront
211         // Check that main contact list has been created
212     if (!defaultController) {
213                 [self loadDetachedGroups];
214     }
215         
216         // Bring all detached windows to front as well
217         NSEnumerator *i = [contactLists objectEnumerator];
218         AIListWindowController *window;
219         while((window = [i nextObject]))
220                 [window showWindowInFrontIfAllowed:bringToFront];
221         
222         [defaultController showWindowInFrontIfAllowed:bringToFront];
226  * @brief Returns YES if the contact list is visible and in front
227  */
228 - (BOOL)contactListIsVisibleAndMain
230         return ([self contactListIsVisible] &&
231                         [[defaultController window] isMainWindow]);
235  * @brief Returns YES if hte contact list is visible
236  */
237 - (BOOL)contactListIsVisible
239         return (defaultController &&
240                         [[defaultController window] isVisible] &&
241                         ([defaultController windowSlidOffScreenEdgeMask] == AINoEdges));
245  * @brief Close contact list
246  */
247 - (void)closeContactList
249         // Close main window
250     if (defaultController)
251                 [[defaultController window] performClose:nil];
252         
253         [self saveAndCloseDetachedGroups];
254         
255         // So that in the future detached windows will reopen as well
256         hasLoaded = NO;
260  * @brief Closes all detached contact lists
261  */
262 - (void)closeDetachedContactLists
264         // Close all other windows
265         NSEnumerator *windowEnumerator = [contactLists objectEnumerator];
266         AIListWindowController *window;
267         while ((window = [windowEnumerator nextObject])) {
268                 [self closeContactList:window];
269         }
273  * @brief Callback when the contact list closes, clear our reference to it
274  */
275 - (void)contactListDidClose:(NSNotification *)notification
277         AIListWindowController *window = [notification object]; 
278         
279         if (window == defaultController) {
280                 [defaultController release];
281                 defaultController = nil;
282         } else {
283                 NSEnumerator *i = [[[window contactList] containedObjects] objectEnumerator];
284                 AIListGroup *group;
285                 AIListObject<AIContainingObject> *contactList;
286                 
287                 contactList = [[adium contactController] contactList];
288                 
289                 while ((group = [i nextObject])){
290                         [group moveGroupTo:contactList];
291                 }
293                 [[adium contactController] removeDetachedContactList:(AIListGroup *)[window contactList]];
294                 
295                 [[adium notificationCenter] postNotificationName:@"Contact_ListChanged"
296                                                                                                   object:contactList 
297                                                                                                 userInfo:nil];
298                         
299                 [contactLists removeObject:window];
300                 
301                 [window release];
302         }
303         
306 //Navigate Through Detached Windows ------------------------------------------------------------------------------------
307 #pragma mark Navigate Through Detached Windows
310  * @return Returns YES if contact list is allowed to be detached
311  */
312 - (BOOL)allowDetachableContactList {
313         return detachable;
317  * @retrun Returns the number of detached contact lists
318  */
319 - (unsigned)detachedContactListCount {
320         return [contactLists count];
324  * @brief Attempts to bring the next detached contact list to the front 
325  */
326 - (void)nextDetachedContactList {
327         if (detachedCycle>=[contactLists count] || detachedCycle<0)
328                 detachedCycle = 0;
329         if (detachedCycle>=0 && detachedCycle<[contactLists count])
330                 [[contactLists objectAtIndex:detachedCycle++] showWindowInFrontIfAllowed:YES];
334  * @brief Attempts to bring the previous detached contact list to the front 
335  */
336 - (void)previousDetachedContactList {
337         if (detachedCycle<0 || detachedCycle>=[contactLists count])
338                 detachedCycle = [contactLists count]-1;
339         if (detachedCycle>=0 && detachedCycle<[contactLists count])
340                 [[contactLists objectAtIndex:detachedCycle--] showWindowInFrontIfAllowed:YES];
343 //Context menu ---------------------------------------------------------------------------------------------------------
344 #pragma mark Context menu
347  * @brief Detaches group if not the only group in contact list
348  * 
349  * @param sender Menu item selected, either to detach or attach group selected
350  */
351 - (IBAction)detachOrAttachMenuAction:(id)sender{
352         AIListGroup             *group;         // Group to be moved
353         AIListGroup             *destContactList;               // Possible new contact list that will be created
354         AIListGroup             *origContactList;       // Contact list group was in originaly
355         
356         // If no group is selected then return
357         if (!(group = (AIListGroup*)[[adium interfaceController] selectedListObject]))
358                 return;
359         
360         // If context menu is clicked do nothing too
361         if (sender == contextSubmenu)
362                 return;
363         
364         
365         origContactList = (AIListGroup *)[group containingObject];
366         
367         // Determine where to move selected group to
368         if (sender == contextMenuDetach) {
369                 destContactList = [[adium contactController] createDetachedContactList];
370         } else {
371                 destContactList = [contextMenuAttach objectForKey:[NSValue valueWithPointer:sender]];
372         }
373         
374         [group moveGroupTo:destContactList];
375         
376         // If detaching group, create new window
377         if(sender == contextMenuDetach)
378                 [[[self detachContactList:destContactList] window] setFrameTopLeftPoint:[NSEvent mouseLocation]];
380         // Update contact list
381         [[adium notificationCenter] postNotificationName:@"Contact_ListChanged"
382                                                                                                                 object:destContactList
383                                                                                                           userInfo:nil];
384         
385         // Update/remove original contact list 
386         if (![origContactList containedObjectsCount]) { 
387                 [[adium notificationCenter] postNotificationName:DetachedContactListIsEmpty
388                                                                                                   object:origContactList
389                                                                                                 userInfo:nil];
390         } else {
391                 [[adium notificationCenter] postNotificationName:@"Contact_ListChanged"
392                                                                                                   object:origContactList
393                                                                                                 userInfo:nil]; 
394         }
397 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem {
398         // If not the main context menu item assume its active
399         if (menuItem == menuItem_nextDetached || menuItem == menuItem_previousDetached || menuItem == menuItem_consolidate)
400                 return ([self detachedContactListCount] != 0);
401         else if (menuItem == contextSubmenu) {
402 #warning Unacceptable and broken
403                 return [self rebuildContextMenu];
404         }
405         return YES;
408 - (BOOL)rebuildContextMenu{     
409         AIListObject *listObject = [[adium interfaceController] selectedListObject];
410         
411         // If no item selected then we can't continue
412         if(listObject == nil)
413                 return NO;
414         
415         NSMutableDictionary *attachMenu = [[NSMutableDictionary alloc] init];
416         
417         // Reset submenu
418         if (contextSubmenuContent == nil) {
419                 contextSubmenuContent = [[NSMenu allocWithZone:[NSMenu menuZone]] initWithTitle:@""];
420         } else {
421                 while ([contextSubmenuContent numberOfItems])
422                         [contextSubmenuContent removeAllItems];
423         }
424         
425         // Attach-to options
426         NSEnumerator *windows = [contactLists objectEnumerator];
427         AIListWindowController  *window;
428         while ((window = [windows nextObject])) {
429                 if (![[window contactList] containsObject:listObject]) {        
430                         NSString *desc = [self formatContextMenu:[window contactList]]; 
432                         NSMenuItem *item = [[NSMenuItem allocWithZone:[NSMenu menuZone]] 
433                                                                                         initWithTitle:desc
434                                                                                                    target:self
435                                                                                                    action:@selector(detachOrAttachMenuAction:)
436                                                                                         keyEquivalent:@""];
438                         [attachMenu setObject:[window contactList] forKey:[NSValue valueWithPointer:item]];
439                         
440                         [contextSubmenuContent addItem:item];
441                         [item release];
442                 }
443         }
444         
445         // Attach to main window -- if using the standard window
446         NSMenuItem *toMain = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Attach to Main Contact List", nil)
447                                                                                                                                                         target:self
448                                                                                                                                                         action:@selector(detachOrAttachMenuAction:)
449                                                                                                                                          keyEquivalent:@""];
450         AIListObject<AIContainingObject> *mainContactList = [[adium contactController] contactList];
451         [attachMenu setObject:mainContactList
452                                    forKey:[NSValue valueWithPointer:toMain]];
453         // Change depending on window style
454         if (windowStyle == AIContactListWindowStyleStandard && [contextSubmenuContent numberOfItems]) {
455                 [contextSubmenuContent insertItem:[NSMenuItem separatorItem] atIndex:0];
456         } else {
457                 [toMain setTitle:[self formatContextMenu:mainContactList]];
458         }
459         // Disable if selected group is part of contact list
460         if ([mainContactList containsObject:listObject])
461                 [toMain setAction:nil];
462         [contextSubmenuContent insertItem:toMain atIndex:0];
463         [toMain release];
464         
465         
466         // Detach option
467         if (contextMenuDetach == nil) {
468                 contextMenuDetach = [[NSMenuItem allocWithZone:[NSMenu menuZone]]
469                                 initWithTitle:AILocalizedString(@"Detach", "menu item title for detaching a group from the contact list")
470                                            target:self
471                                            action:@selector(detachOrAttachMenuAction:)
472                                 keyEquivalent:@""];
473         }
474         [contextSubmenuContent addItem:[NSMenuItem separatorItem]];
475         [contextSubmenuContent addItem:contextMenuDetach];
477         // Add submenu if not yet added
478         if ([contextSubmenu submenu] == nil)
479                 [contextSubmenu setSubmenu:contextSubmenuContent];
480                 
481         // List of attach locations
482         if (contextMenuAttach!=nil)
483                 [contextMenuAttach release];
484         contextMenuAttach = attachMenu;
485         
486         // Respect if groups can detach
487         if (![self allowDetachableContactList])
488                 return NO;
489         else
490                 return YES;
491         
494 - (NSString *)formatContextMenu:(AIListObject<AIContainingObject> *)contactList {
495         NSString *description = [self formatContextMenu:contactList showEmpty:NO];
496         if (description == nil)
497                 return [self formatContextMenu:contactList showEmpty:YES];
498         return description;
501 - (NSString *)formatContextMenu:(AIListObject<AIContainingObject> *)contactList showEmpty:(BOOL)empty {
502         NSArray *groups = [contactList containedObjects];
503         NSString *desc = @"";
504         
505         unsigned count=0;
506         for (unsigned i=0;count<3 && i<[groups count]; i++) {
507                 if ([[groups objectAtIndex:i] visible] || empty) {
508                         if (count)
509                                 desc = [desc stringByAppendingFormat:@", %@",[[groups objectAtIndex:i] displayName]];
510                         else 
511                                 desc = [desc stringByAppendingFormat:AILocalizedString(@"Attach to %@", "Menu item for attaching one contact list group to another window. %@ will be a group name"),
512                                                 [[groups objectAtIndex:i] displayName]];
513                         count++;
514                 }
515         }
516         if (count>2 && [groups count]>3)
517                 desc = [desc stringByAppendingEllipsis];
518         
519         if (!count) 
520                 return  nil;
521         return desc;
522         
525 //Themes and Layouts --------------------------------------------------------------------------------------------------
526 #pragma mark Contact List Controller
527 //Apply any theme/layout changes
528 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
529                                                         object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
530 {       
531         if ([group isEqualToString:PREF_GROUP_APPEARANCE]) {
532                 if (firstTime || !key || [key isEqualToString:KEY_LIST_LAYOUT_WINDOW_STYLE]) {
533                         int     newWindowStyle = [[prefDict objectForKey:KEY_LIST_LAYOUT_WINDOW_STYLE] intValue];
534                         
535                         if (newWindowStyle != windowStyle) {
536                                 windowStyle = newWindowStyle;
537                                 
538                                 //If a contact list is visible and the window style has changed, update for the new window style
539                                 if (defaultController) {
540                                         //XXX - Evan: I really do not like this at all.  What to do?
541                                         //We can't close and reopen the contact list from within a preferencesChanged call, as the
542                                         //contact list itself is a preferences observer and will modify the array for its group as it
543                                         //closes... and you can't modify an array while enuemrating it, which the preferencesController is
544                                         //currently doing.  This isn't pretty, but it's the most efficient fix I could come up with.
545                                         //It has the obnoxious side effect of the contact list changing its view prefs and THEN closing and
546                                         //reopening with the right windowStyle.
547                                         [self performSelector:@selector(closeAndReopencontactList)
548                                                            withObject:nil
549                                                            afterDelay:0.00001];
550                                 }
551                         }
552                 }
553         }
557  * @brief Closes main contact list and reopens it
559  * Useful for updating settings and data of the main contact list
560  */
561 - (void)closeAndReopencontactList
563         BOOL isVisibleAndMain = [self contactListIsVisibleAndMain];
565         [self saveAndCloseDetachedGroups];
567         hasLoaded = NO;
568         [defaultController close];
569         [defaultController release]; defaultController = nil;
571         [self showContactListAndBringToFront:isVisibleAndMain];
574 // Preferences --------------------------------------------------------------------------------------------------------
575 #pragma mark Preferences
577  * @brief Saves location of contact list and information about the detached groups
578  */
579 - (void)saveAndCloseDetachedGroups
580 {               
581         NSMutableArray *detachedWindowsDicts = [[NSMutableArray alloc] init];
582         NSEnumerator *enumerator = [[[contactLists copy] autorelease] objectEnumerator];
583         AIListWindowController *windowController;
585         while ((windowController = [enumerator nextObject])) {
586                 NSMutableDictionary *dict = [NSDictionary dictionaryWithObject:[[[windowController contactList] containedObjects] valueForKey:@"UID"]
587                                                                                                                                 forKey:DETACHED_WINDOW_GROUPS];
588                 [detachedWindowsDicts addObject:dict];
589                 [self closeContactList:windowController];
590         }
592         [[adium preferenceController] setPreference:detachedWindowsDicts
593                                                                                  forKey:DETACHED_WINDOWS
594                                                                                   group:PREF_DETACHED_GROUPS];
595         [detachedWindowsDicts release];
599  * @brief Loads main contact list window if not already loaded and if this 
600  * is the first time that that we are loading the contact list we detached
601  * groups and place them in the correct location
602  */
603 - (void)loadDetachedGroups
605         if (!defaultController && windowStyle == AIContactListWindowStyleStandard) {
606                 defaultController = [[AIStandardListWindowController listWindowController] retain];
607         } else if (!defaultController) {
608                 defaultController = [[AIBorderlessListWindowController listWindowController] retain];
609         }
610         
611         if (!hasLoaded && detachable) {
612                 NSArray *detachedWindowsDict = [[adium preferenceController] preferenceForKey:DETACHED_WINDOWS
613                                                                                                                                                                 group:PREF_DETACHED_GROUPS];
614                 NSEnumerator *enumerator = [detachedWindowsDict objectEnumerator];
615                 NSDictionary *windowPreferenceDict;
616                 
617                 while ((windowPreferenceDict = [enumerator nextObject])) {
618                         [self loadWindowPreferences:windowPreferenceDict];
619                 }
620                 
621                 hasLoaded = YES;
622         }
627  * @brief Loads detached window based on saved preferences
628  */
629 - (void)loadWindowPreferences:(NSDictionary *)windowPreferences
631         AIListGroup             *contactList = nil;
632         NSArray                 *groups = [windowPreferences objectForKey:DETACHED_WINDOW_GROUPS];
633         NSString                *groupUID;
634         NSEnumerator    *enumerator;
636         if (![groups count])
637                 return;
639         contactList = [[adium contactController] createDetachedContactList];
641         enumerator = [groups objectEnumerator];
642         while ((groupUID = [enumerator nextObject])) {
643                 AIListGroup             *group = [[adium contactController] groupWithUID:groupUID];
644                 [group moveGroupTo:contactList];
645         }
646         
647         [self detachContactList:contactList];
650 @end