* Adium is the legal property of its developers, whose names are listed in the copyright file included * with this source distribution. * This program is free software; you can redistribute it and/or modify it under the terms of the GNU * General Public License as published by the Free Software Foundation; either version 2 of the License, * or (at your option) any later version. * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. * You should have received a copy of the GNU General Public License along with this program; if not, * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #import <Adium/AIAccountControllerProtocol.h> #import <Adium/AIChatControllerProtocol.h> #import <Adium/AIInterfaceControllerProtocol.h> #import <Adium/AIStatusControllerProtocol.h> #import <Adium/AIContactControllerProtocol.h> #import <Adium/AIListObject.h> #import "CBStatusMenuItemPlugin.h" #import "CBStatusMenuItemController.h" #import "AIMenuBarIcons.h" #import <AIUtilities/AIMenuAdditions.h> #import <AIUtilities/AIEventAdditions.h> #import <AIUtilities/AIArrayAdditions.h> #import <AIUtilities/AIImageAdditions.h> #import <Adium/AIAccount.h> #import <Adium/AIListContact.h> #import <Adium/AIListBookmark.h> #import <Adium/AIStatusIcons.h> #import <Adium/AIContactHidingController.h> #import <AIUtilities/AIColorAdditions.h> #import <AIUtilities/AIStringAdditions.h> // For the KEY_SHOW_OFFLINE_CONTACTS and PREF_GROUP_CONTACT_LIST_DISPLAY #import "AIContactController.h" #import "AIInterfaceController.h" #define STATUS_ITEM_MARGIN 8 @interface CBStatusMenuItemController () - (NSImage *)badgeDuck:(NSImage *)duckImage withImage:(NSImage *)inImage; - (void)updateMenuIconsBundle; - (void)updateUnreadCount; - (void)updateStatusItemLength; - (void)switchToChat:(id)sender; - (void)activateAccountList:(id)sender; - (void)disableStatusItem:(id)sender; @property (nonatomic, retain) NSMenuItem *contactsMenuItem; @implementation CBStatusMenuItemController @synthesize contactsMenuItem; + (CBStatusMenuItemController *)statusMenuItemController return [[[self alloc] init] autorelease]; if ((self = [super init])) { //Create and set up the status item statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:25] retain]; statusItemView = [[AIStatusItemView alloc] initWithFrame:NSMakeRect(0,0,25,22)]; statusItemView.statusItem = statusItem; [statusItem setView:statusItemView]; [self updateMenuIconsBundle]; mainMenu = [[NSMenu alloc] init]; [mainMenu setDelegate:self]; mainAccountsMenu = [[NSMenu alloc] init]; [mainAccountsMenu setDelegate:self]; mainOptionsMenu = [[NSMenu alloc] init]; [mainOptionsMenu setDelegate:self]; // Set the main menu as the status item's menu statusItemView.menu = mainMenu; // Flag all the menus as needing updates mainMenuNeedsUpdate = YES; contactsMenuNeedsUpdate = YES; accountsMenuNeedsUpdate = YES; optionsMenuNeedsUpdate = YES; self.contactsMenuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Contacts",nil) keyEquivalent:@""] autorelease]; NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; //Register to recieve chat opened and chat closed notifications [notificationCenter addObserver:self selector:@selector(updateOpenChats) [notificationCenter addObserver:self selector:@selector(updateOpenChats) [notificationCenter addObserver:self selector:@selector(updateOpenChats) [notificationCenter addObserver:self selector:@selector(updateMenuIcons) name:AIStatusIconSetDidChangeNotification // Register for our menu bar icon set changing [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateMenuIconsBundle) name:AIMenuBarIconsDidChangeNotification // Register as a chat observer so we can know the status of unread messages [adium.chatController registerChatObserver:self]; // Register as a list object observer so we can know when accounts need to show reconnecting [[AIContactObserverManager sharedManager] registerListObjectObserver:self]; // Register as an observer of the preference group so we can update our "show groups contacts" option [adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_CONTACT_LIST_DISPLAY]; // Register as an observer of the status preferences for unread conversation count [adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_STATUS_PREFERENCES]; // Register as an observer of our own preference group [adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_STATUS_MENU_ITEM]; //Register to recieve active state changed notifications [notificationCenter addObserver:self selector:@selector(updateMenuIcons) name:AIStatusActiveStateChangedNotification //Register ourself for the status menu items statusMenu = [[AIStatusMenu statusMenuWithDelegate:self] retain]; accountMenu = [[AIAccountMenu accountMenuWithDelegate:self submenuType:AIAccountStatusSubmenu showTitleVerbs:YES] retain]; contactMenu = [[AIContactMenu contactMenuWithDelegate:self forContactsInObject:nil] retain]; // Invalidate and release our timers [[AIContactObserverManager sharedManager] unregisterListObjectObserver:self]; [adium.chatController unregisterChatObserver:self]; [[NSNotificationCenter defaultCenter] removeObserver:self]; [adium.preferenceController unregisterPreferenceObserver:self]; [[statusItem statusBar] removeStatusItem:statusItem]; [statusItemView release]; // All the temporary NSMutableArrays we store [accountMenuItemsArray release]; [stateMenuItemsArray release]; [openChatsArray release]; [mainAccountsMenu release]; [mainOptionsMenu release]; // Release our various menus. [accountMenu setDelegate:nil]; [accountMenu release]; [contactMenu setDelegate:nil]; [contactMenu release]; [statusMenu setDelegate:nil]; [statusMenu release]; // Release our AIMenuBarIcons bundle // Can't release this because it causes a crash on quit. rdar://4139755, rdar://4160625, and #743. --boredzo // Or when toggling the menu item. #16381 --wixardy //To the superclass, Robin! #define PREF_GROUP_APPEARANCE @"Appearance" #define KEY_MENU_BAR_ICONS @"Menu Bar Icons" #define EXTENSION_MENU_BAR_ICONS @"AdiumMenuBarIcons" #define RESOURCE_MENU_BAR_ICONS @"Menu Bar Icons" * @brief Update the Xtra bundle * Updates the stored information we have on an \c AdiumMenuBarIcons bundle. - (void)updateMenuIconsBundle NSString *menuIconPath = nil, *menuIconName; menuIconName = [adium.preferenceController preferenceForKey:KEY_MENU_BAR_ICONS group:PREF_GROUP_APPEARANCE // Get the path of the pack if found. menuIconPath = [adium pathOfPackWithName:menuIconName extension:EXTENSION_MENU_BAR_ICONS resourceFolderName:RESOURCE_MENU_BAR_ICONS]; // If the pack is not found, get the default one. if (!menuIconPath || !menuIconName) { menuIconName = [adium.preferenceController defaultPreferenceForKey:KEY_MENU_BAR_ICONS group:PREF_GROUP_APPEARANCE menuIconPath = [adium pathOfPackWithName:menuIconName extension:EXTENSION_MENU_BAR_ICONS resourceFolderName:RESOURCE_MENU_BAR_ICONS]; menuIcons = [[AIMenuBarIcons alloc] initWithURL:[NSURL fileURLWithPath:menuIconPath]]; * @brief Update the unread count * Updates the string text found next to the status item's icon. - (void)updateUnreadCount NSUInteger unreadCount = (showConversationCount ? [adium.chatController unviewedConversationCount] : [adium.chatController unviewedContentCount]); // Only show if enabled and greater-than zero; otherwise, set to nil. if (showUnreadCount && unreadCount > 0) { [statusItemView setStringValue:[NSString stringWithFormat:@"%lu", unreadCount]]; [statusItemView setStringValue:nil]; * @brief Update the unviewed content flash * @arg timer The NSTimer calling this method * Toggles state between having unread content and not every time the timer ends. - (void)updateUnviewedContentFlash:(NSTimer *)timer // Invert our current setting currentlyIgnoringUnviewed = !currentlyIgnoringUnviewed; // Update our current menu icon * @brief Invalidate running timers * Since an NSTimer instance retains its targets, this method is used to prevent * \c autoreleased objects from being stuck around indefinitely. currentlyIgnoringUnviewed = NO; [unviewedContentFlash invalidate]; [unviewedContentFlash release]; unviewedContentFlash = nil; #define IMAGE_TYPE_CONTENT @"Content" #define IMAGE_TYPE_AWAY @"Away" #define IMAGE_TYPE_IDLE @"Idle" #define IMAGE_TYPE_INVISIBLE @"Invisible" #define IMAGE_TYPE_OFFLINE @"Offline" #define IMAGE_TYPE_ONLINE @"Online" * @brief Update the menu icons * Updates the menu icon with the appropriate icon and badge icon. // If there's content, set our badge to the "content" icon. if (unviewedContent && !currentlyIgnoringUnviewed) { badge = [AIStatusIcons statusIconForStatusName:@"content" statusType:AIAvailableStatusType iconType:AIStatusIconList imageName = IMAGE_TYPE_CONTENT; // Get the correct icon for our current state. switch([adium.statusController.activeStatusState statusType]) { badge = [adium.statusController.activeStatusState icon]; imageName = IMAGE_TYPE_AWAY; case AIInvisibleStatusType: badge = [adium.statusController.activeStatusState icon]; imageName = IMAGE_TYPE_INVISIBLE; case AIOfflineStatusType: imageName = IMAGE_TYPE_OFFLINE; // Assuming we're using an online image unless proven otherwise imageName = IMAGE_TYPE_ONLINE; // Check idle here, since it has less precedence than offline, invisible, or away. for (AIAccount *account in adium.accountController.accounts) { if (account.online && [account valueForProperty:@"idleSince"]) { badge = [AIStatusIcons statusIconForStatusName:@"Idle" statusType:AIAvailableStatusType iconType:AIStatusIconList imageName = IMAGE_TYPE_IDLE; NSImage *menuIcon = [menuIcons imageOfType:imageName alternate:NO]; NSImage *alternateMenuIcon = [menuIcons imageOfType:imageName alternate:YES]; statusItemView.regularImage = [self badgeDuck:menuIcon withImage:badge]; // Badge the highlight image and set it. statusItemView.alternateImage = [self badgeDuck:alternateMenuIcon withImage:badge]; // Update our unread count. [self updateUnreadCount]; // Update the status item length [self updateStatusItemLength]; * @brief Update the status item's width - (void)updateStatusItemLength [statusItem setLength:statusItemView.desiredWidth + STATUS_ITEM_MARGIN]; [statusItemView setFrame:NSMakeRect(0, 0, statusItemView.desiredWidth + STATUS_ITEM_MARGIN, 22)]; [statusItemView setNeedsDisplay:YES]; * @brief Badge the given image with the given badge * @arg duckImage The base image * @arg badgeImage The badge which will be draw on the base image * Draws the \c badgeImage in the bottom right quadrant of the \c duckImage. - (NSImage *)badgeDuck:(NSImage *)duckImage withImage:(NSImage *)badgeImage NSImage *image = duckImage; image = [[duckImage copy] autorelease]; NSRect srcRect = { NSZeroPoint, [badgeImage size] }; //Draw in the lower-right quadrant. { .x = srcRect.size.width, .y = 0.0f }, destRect.size.width *= 0.5f; destRect.size.height *= 0.5f; //If the badge is bigger than that portion, resize proportionally. Otherwise, leave it alone and adjust the destination origin appropriately. if ((srcRect.size.width > destRect.size.width) || (srcRect.size.height > destRect.size.height)) { if (srcRect.size.width > srcRect.size.height) { scale = destRect.size.width / srcRect.size.width; scale = destRect.size.height / srcRect.size.height; destRect.size.width = srcRect.size.width * scale; destRect.size.height = srcRect.size.height * scale; //Make sure we scale in a pretty manner. [[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh]; //Move the drawing origin. destRect.origin.x = [duckImage size].width - destRect.size.width; [badgeImage drawInRect:destRect operation:NSCompositeSourceOver #pragma mark Account Menu * @brief AIAccountMenu delegate method - (void)accountMenu:(AIAccountMenu *)inAccountMenu didRebuildMenuItems:(NSArray *)menuItems { // Going from or to 1 account requires a main menu update if ([accountMenuItemsArray count] == 1 || [menuItems count] == 1) mainMenuNeedsUpdate = YES; [accountMenuItemsArray release]; accountMenuItemsArray = [menuItems retain]; //We need to update next time we're clicked accountsMenuNeedsUpdate = YES; * @brief AIAccountMenu delegate method - (void)accountMenu:(AIAccountMenu *)inAccountMenu didSelectAccount:(AIAccount *)inAccount { [inAccount toggleOnline]; * @brief AIStatusMenu delegate method - (void)statusMenu:(AIStatusMenu *)inStatusMenu didRebuildStatusMenuItems:(NSArray *)menuItemArray [stateMenuItemsArray release]; stateMenuItemsArray = [menuItemArray retain]; //We need to update next time we're clicked mainMenuNeedsUpdate = YES; #pragma mark Contact Menu * @brief AIContactMenu delegate method - (void)contactMenuDidRebuild:(AIContactMenu *)inContactMenu NSMenu *menu = inContactMenu.menu; NSInteger newNumberOfMenuItems = menu.numberOfItems; // Going from or to 0 contacts requires a main menu update if (currentContactMenuItemsCount == 0 || newNumberOfMenuItems == 0) mainMenuNeedsUpdate = YES; currentContactMenuItemsCount = menu.numberOfItems; /* The alternate menu is what shows if you option-click the menu item */ statusItemView.alternateMenu = menu; [self.contactsMenuItem setSubmenu:menu]; * @brief AIContactMenu delegate method - (void)contactMenu:(AIContactMenu *)inContactMenu didSelectContact:(AIListContact *)inContact [adium.interfaceController setActiveChat:[adium.chatController openChatWithContact:inContact onPreferredAccount:YES]]; * @brief AIContactMenu delegate method * Shows the given contact if it is visible in the contact list. - (BOOL)contactMenu:(AIContactMenu *)inContactMenu shouldIncludeContact:(AIListContact *)inContact // Show this contact if we're showing offline contacts or if this contact is online. for (id<AIContainingObject> container in inContact.containingObjects) if ([[AIContactHidingController sharedController] visibilityOfListObject:inContact inContainer:container]) * @brief AIContactMenu delegate method - (BOOL)contactMenuShouldDisplayGroupHeaders:(AIContactMenu *)inContactMenu return showContactGroups; * @brief AIContactMenu delegate method - (BOOL)contactMenuShouldUseDisplayName:(AIContactMenu *)inContactMenu * @brief AIContactMenu delegate method - (BOOL)contactMenuShouldUseUserIcon:(AIContactMenu *)inContactMenu * @brief AIContactMenu delegate method - (BOOL)contactMenuShouldSetTooltip:(AIContactMenu *)inContactMenu - (BOOL) contactMenuShouldIncludeContactListMenuItem:(AIContactMenu *)inContactMenu - (BOOL)contactMenuShouldPopulateMenuLazily:(AIContactMenu *)inContactMenu #pragma mark List Object Observer * @brief List Observer delegate method * Updates the menu icon if our accounts change connecting state. - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent if ([inObject isKindOfClass:[AIAccount class]]) { if ([inModifiedKeys containsObject:@"isConnecting"] || [inModifiedKeys containsObject:@"waitingToReconnect"]) { #pragma mark Chat Observer * @brief Chat observer delegate method * Updates our opened chats when called. - (NSSet *)updateChat:(AIChat *)inChat keys:(NSSet *)inModifiedKeys silent:(BOOL)silent // We didn't modify anything; return nil. * @brief Updates open chats menu * Update our content image if necessary, creating an NSTimer instance to flash the badge if the * user has the preference enabled to do so. NSUInteger unviewedContentCount = [adium.chatController unviewedContentCount]; [openChatsArray release]; openChatsArray = [[adium.interfaceController openChats] retain]; // We think there's unviewed content, but there's not. if (unviewedContent && unviewedContentCount == 0) { // Invalidate and release the unviewed content flash timer [unviewedContentFlash invalidate]; [unviewedContentFlash release]; unviewedContentFlash = nil; currentlyIgnoringUnviewed = NO; // Update unviewed content // We think there's no unviewed content, and there is. } else if (!unviewedContent && unviewedContentCount > 0) { // If this particular Xtra wants us to flash unviewed content, start the timer up currentlyIgnoringUnviewed = NO; unviewedContentFlash = [[NSTimer scheduledTimerWithTimeInterval:1.0 selector:@selector(updateUnviewedContentFlash:) // Update unviewed content // If we already know there's unviewed content, just update the count. } else if (unviewedContent && unviewedContentCount > 0) { [self updateUnreadCount]; mainMenuNeedsUpdate = YES; #pragma mark Menu Delegates/Actions * @brief NSMenu delegate method * This method updates all of the given menus which we control if we've deteremined an update to be necessary. - (void)menuNeedsUpdate:(NSMenu *)menu // Main menu if it needs an update if (menu == mainMenu && mainMenuNeedsUpdate) { //Clear out all the items, start from scratch // Show the contacts menu if we have any contacts to display if ([contactMenu.menu numberOfItems] > 0) { [menu addItem:self.contactsMenuItem]; [menu addItemWithTitle:[AILocalizedString(@"Contact List", nil) stringByAppendingEllipsis] target:adium.interfaceController action:@selector(toggleContactList:) // If there's more than one account, show the accounts menu if ([accountMenuItemsArray count] > 1) { menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Accounts",nil) [menuItem setSubmenu:mainAccountsMenu]; menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Options",nil) [menuItem setSubmenu:mainOptionsMenu]; [menu addItem:[NSMenuItem separatorItem]]; //Add the state menu items for (menuItem in stateMenuItemsArray) { //Validate the menu items as they are added since they weren't previously validated when the menu was clicked if ([[menuItem target] respondsToSelector:@selector(validateMenuItem:)]) { [[menuItem target] validateMenuItem:menuItem]; //If there exist any open chats, add them if ([openChatsArray count] > 0) { [menu addItem:[NSMenuItem separatorItem]]; //Create and add the menu items for (AIChat *chat in openChatsArray) { //Create a menu item from the chat menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:chat.displayName action:@selector(switchToChat:) //Set the represented object [menuItem setRepresentedObject:chat]; //If there is a chat status image, use that image = [AIStatusIcons statusIconForChat:chat type:AIStatusIconMenu direction:AIIconNormal]; //Otherwise use the chat's -chatMenuImage image = [chat chatMenuImage]; [menuItem setImage:image]; //Only update next time if we need to mainMenuNeedsUpdate = NO; } else if (menu == mainAccountsMenu && accountsMenuNeedsUpdate) { [menu addItemWithTitle:[AILocalizedString(@"Account List", nil) stringByAppendingEllipsis] action:@selector(activateAccountList:) [menu addItem:[NSMenuItem separatorItem]]; //Add the account menu items for (menuItem in accountMenuItemsArray) { //Validate the menu items as they are added since they weren't previously validated when the menu was clicked if ([[menuItem target] respondsToSelector:@selector(validateMenuItem:)]) { [[menuItem target] validateMenuItem:menuItem]; if ((submenu = [menuItem submenu])) { for (NSMenuItem *submenuItem in submenu.itemArray) { //Validate the submenu items as they are added since they weren't previously validated when the menu was clicked if ([[submenuItem target] respondsToSelector:@selector(validateMenuItem:)]) { [[submenuItem target] validateMenuItem:submenuItem]; accountsMenuNeedsUpdate = NO; } else if (menu == mainOptionsMenu && optionsMenuNeedsUpdate) { [menu addItemWithTitle:[AILocalizedString(@"Adium Preferences", nil) stringByAppendingEllipsis] action:@selector(showPreferenceWindow:) [menu addItemWithTitle:AILocalizedString(@"Toggle Contact List", nil) target:adium.interfaceController action:@selector(toggleContactList:) [menu addItem:[NSMenuItem separatorItem]]; [menu addItemWithTitle:AILocalizedString(@"Hide Status Item", nil) action:@selector(disableStatusItem:) [menu addItemWithTitle:AILocalizedString(@"Quit Adium", nil) action:@selector(terminate:) optionsMenuNeedsUpdate = NO; * @brief Switch to a chat * @arg An NSMenuItem instance whose \c representedObject is an AIChat. - (void)switchToChat:(id)sender [adium.interfaceController setActiveChat:[sender representedObject]]; * @brief Open the account list - (void)activateAccountList:(id)sender [adium.preferenceController openPreferencesToCategoryWithIdentifier:@"Accounts"]; * @brief Disable the status item * Updates the preference for displaying the status item to be NO. - (void)disableStatusItem:(id)sender [adium.preferenceController setPreference:[NSNumber numberWithBool:NO] forKey:KEY_STATUS_MENU_ITEM_ENABLED group:PREF_GROUP_STATUS_MENU_ITEM]; * @brief Show the preference window - (void)showPreferenceWindow:(id)sender [adium.preferenceController showPreferenceWindow:nil]; [NSApp activateIgnoringOtherApps:YES]; [NSApp arrangeInFront:nil]; #pragma mark Preferences Observer * @brief Preferences observer * Updates our display based on preference changes, such as the display of badges or the unread count being displayed. - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime if ([group isEqualToString:PREF_GROUP_CONTACT_LIST_DISPLAY]) { showContactGroups = ![[prefDict objectForKey:KEY_HIDE_CONTACT_LIST_GROUPS] boolValue]; [contactMenu rebuildMenu]; if ([group isEqualToString:PREF_GROUP_STATUS_MENU_ITEM]) { showUnreadCount = [[prefDict objectForKey:KEY_STATUS_MENU_ITEM_COUNT] boolValue]; showBadge = [[prefDict objectForKey:KEY_STATUS_MENU_ITEM_BADGE] boolValue]; flashUnviewed = [[prefDict objectForKey:KEY_STATUS_MENU_ITEM_FLASH] boolValue]; [self updateUnreadCount]; [self updateStatusItemLength]; if ([group isEqualToString:PREF_GROUP_STATUS_PREFERENCES]) { showConversationCount = [[prefDict objectForKey:KEY_STATUS_CONVERSATION_COUNT] boolValue]; [self updateUnreadCount];