2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
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.
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.
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.
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;
47 * @class AISCLViewPlugin
48 * @brief This component plugin is responsible for controlling the main contact list and detached contact lists window and view.
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.
54 * In either case, the outline view itself is controlled by an instance of AIListController.
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.
59 @implementation AISCLViewPlugin
64 contactLists = [[NSMutableArray alloc] init];
66 [[adium interfaceController] registerContactListController:self];
68 //Install our preference view
69 advancedPreferences = [[ESContactListAdvancedPreferences preferencePane] retain];
72 contextSubmenu = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Attach / Detach", "Menu item for attaching and detatching groups")
74 action:@selector(detachOrAttachMenuAction:)
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")
82 action:@selector(closeDetachedContactLists)
84 [[adium menuController] addMenuItem:menuItem_consolidate toLocation:LOC_Window_Commands];
85 menuItem_nextDetached = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Next Detached Group", "menu item title")
87 action:@selector(nextDetachedContactList)
89 [[adium menuController] addMenuItem:menuItem_nextDetached toLocation:LOC_Window_Commands];
90 menuItem_previousDetached = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Previous Detached Group", "menu item title")
92 action:@selector(previousDetachedContactList)
94 [[adium menuController] addMenuItem:menuItem_previousDetached toLocation:LOC_Window_Commands];
97 //Observe list closing
98 [[adium notificationCenter] addObserver:self
99 selector:@selector(contactListDidClose:)
100 name:Interface_ContactListDidClose
103 [[adium notificationCenter] addObserver:self
104 selector:@selector(contactListIsEmpty:)
105 name:DetachedContactListIsEmpty
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];
113 //Observe window style changes
114 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_APPEARANCE];
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];
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
146 - (id)detachContactList:(AIListGroup *)contactList
148 if ([contactList isKindOfClass:[AIListGroup class]]) {
149 AIListWindowController *newContactList = [AIBorderlessListWindowController listWindowControllerForContactList:contactList];
151 [contactLists addObject:[newContactList retain]];
152 [newContactList showWindowInFrontIfAllowed:YES];
154 return newContactList;
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)
167 - (void)closeContactList:(AIListWindowController *)window
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.
179 - (void)contactListIsEmpty:(NSNotification *)notification
181 NSEnumerator *i = [contactLists objectEnumerator];
182 id object = [notification object];
183 AIListWindowController *window;
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];
194 //Contact List Controller ----------------------------------------------------------------------------------------------
195 #pragma mark Contact List Controller
198 * @brief Retrieve the AIListWindowController in use
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
209 - (void)showContactListAndBringToFront:(BOOL)bringToFront
211 // Check that main contact list has been created
212 if (!defaultController) {
213 [self loadDetachedGroups];
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];
222 [defaultController showWindowInFrontIfAllowed:bringToFront];
226 * @brief Returns YES if the contact list is visible and in front
228 - (BOOL)contactListIsVisibleAndMain
230 return ([self contactListIsVisible] &&
231 [[defaultController window] isMainWindow]);
235 * @brief Returns YES if hte contact list is visible
237 - (BOOL)contactListIsVisible
239 return (defaultController &&
240 [[defaultController window] isVisible] &&
241 ([defaultController windowSlidOffScreenEdgeMask] == AINoEdges));
245 * @brief Close contact list
247 - (void)closeContactList
250 if (defaultController)
251 [[defaultController window] performClose:nil];
253 [self saveAndCloseDetachedGroups];
255 // So that in the future detached windows will reopen as well
260 * @brief Closes all detached contact lists
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];
273 * @brief Callback when the contact list closes, clear our reference to it
275 - (void)contactListDidClose:(NSNotification *)notification
277 AIListWindowController *window = [notification object];
279 if (window == defaultController) {
280 [defaultController release];
281 defaultController = nil;
283 NSEnumerator *i = [[[window contactList] containedObjects] objectEnumerator];
285 AIListObject<AIContainingObject> *contactList;
287 contactList = [[adium contactController] contactList];
289 while ((group = [i nextObject])){
290 [group moveGroupTo:contactList];
293 [[adium contactController] removeDetachedContactList:(AIListGroup *)[window contactList]];
295 [[adium notificationCenter] postNotificationName:@"Contact_ListChanged"
299 [contactLists removeObject:window];
306 //Navigate Through Detached Windows ------------------------------------------------------------------------------------
307 #pragma mark Navigate Through Detached Windows
310 * @return Returns YES if contact list is allowed to be detached
312 - (BOOL)allowDetachableContactList {
317 * @retrun Returns the number of detached contact lists
319 - (unsigned)detachedContactListCount {
320 return [contactLists count];
324 * @brief Attempts to bring the next detached contact list to the front
326 - (void)nextDetachedContactList {
327 if (detachedCycle>=[contactLists count] || 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
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
349 * @param sender Menu item selected, either to detach or attach group selected
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
356 // If no group is selected then return
357 if (!(group = (AIListGroup*)[[adium interfaceController] selectedListObject]))
360 // If context menu is clicked do nothing too
361 if (sender == contextSubmenu)
365 origContactList = (AIListGroup *)[group containingObject];
367 // Determine where to move selected group to
368 if (sender == contextMenuDetach) {
369 destContactList = [[adium contactController] createDetachedContactList];
371 destContactList = [contextMenuAttach objectForKey:[NSValue valueWithPointer:sender]];
374 [group moveGroupTo:destContactList];
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
385 // Update/remove original contact list
386 if (![origContactList containedObjectsCount]) {
387 [[adium notificationCenter] postNotificationName:DetachedContactListIsEmpty
388 object:origContactList
391 [[adium notificationCenter] postNotificationName:@"Contact_ListChanged"
392 object:origContactList
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];
408 - (BOOL)rebuildContextMenu{
409 AIListObject *listObject = [[adium interfaceController] selectedListObject];
411 // If no item selected then we can't continue
412 if(listObject == nil)
415 NSMutableDictionary *attachMenu = [[NSMutableDictionary alloc] init];
418 if (contextSubmenuContent == nil) {
419 contextSubmenuContent = [[NSMenu allocWithZone:[NSMenu menuZone]] initWithTitle:@""];
421 while ([contextSubmenuContent numberOfItems])
422 [contextSubmenuContent removeAllItems];
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]]
435 action:@selector(detachOrAttachMenuAction:)
438 [attachMenu setObject:[window contactList] forKey:[NSValue valueWithPointer:item]];
440 [contextSubmenuContent addItem:item];
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)
448 action:@selector(detachOrAttachMenuAction:)
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];
457 [toMain setTitle:[self formatContextMenu:mainContactList]];
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];
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")
471 action:@selector(detachOrAttachMenuAction:)
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];
481 // List of attach locations
482 if (contextMenuAttach!=nil)
483 [contextMenuAttach release];
484 contextMenuAttach = attachMenu;
486 // Respect if groups can detach
487 if (![self allowDetachableContactList])
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];
501 - (NSString *)formatContextMenu:(AIListObject<AIContainingObject> *)contactList showEmpty:(BOOL)empty {
502 NSArray *groups = [contactList containedObjects];
503 NSString *desc = @"";
506 for (unsigned i=0;count<3 && i<[groups count]; i++) {
507 if ([[groups objectAtIndex:i] visible] || empty) {
509 desc = [desc stringByAppendingFormat:@", %@",[[groups objectAtIndex:i] displayName]];
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]];
516 if (count>2 && [groups count]>3)
517 desc = [desc stringByAppendingEllipsis];
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
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];
535 if (newWindowStyle != windowStyle) {
536 windowStyle = newWindowStyle;
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)
557 * @brief Closes main contact list and reopens it
559 * Useful for updating settings and data of the main contact list
561 - (void)closeAndReopencontactList
563 BOOL isVisibleAndMain = [self contactListIsVisibleAndMain];
565 [self saveAndCloseDetachedGroups];
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
579 - (void)saveAndCloseDetachedGroups
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];
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
603 - (void)loadDetachedGroups
605 if (!defaultController && windowStyle == AIContactListWindowStyleStandard) {
606 defaultController = [[AIStandardListWindowController listWindowController] retain];
607 } else if (!defaultController) {
608 defaultController = [[AIBorderlessListWindowController listWindowController] retain];
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;
617 while ((windowPreferenceDict = [enumerator nextObject])) {
618 [self loadWindowPreferences:windowPreferenceDict];
627 * @brief Loads detached window based on saved preferences
629 - (void)loadWindowPreferences:(NSDictionary *)windowPreferences
631 AIListGroup *contactList = nil;
632 NSArray *groups = [windowPreferences objectForKey:DETACHED_WINDOW_GROUPS];
634 NSEnumerator *enumerator;
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];
647 [self detachContactList:contactList];