From d55b3efebe75235ef30c708d8b4aa8552907e693 Mon Sep 17 00:00:00 2001 From: yanas Date: Tue, 12 Nov 2013 22:37:38 +0100 Subject: [PATCH] Adds the possibility to edit account details. Patch provided by Marin Dzhigarov on dev (Nov 8, 2013). --- .classpath | 1 + build.xml | 5 +- lib/felix.client.run.properties | 1 + lib/installer-exclude/jcalendar-1.4.jar | Bin 0 -> 169029 bytes resources/images/images.properties | 1 + .../impl/gui/buttons/accountEditIcon.png | Bin 0 -> 1559 bytes resources/languages/resources.properties | 18 + .../communicator/impl/gui/main/MainFrame.java | 2 +- .../impl/gui/main/account/AccountList.java | 34 + .../main/account/AccountRightButtonMenu.java | 260 ++++ .../gui/main/presence/AccountStatusPanel.java | 10 +- .../impl/gui/swing.ui.manifest.mf | 1 + .../impl/gui/utils/ImageLoader.java | 6 + ...tionSetServerStoredAccountInfoIcqImpl.java | 30 + .../impl/protocol/jabber/InfoRetreiver.java | 39 +- ...nSetServerStoredAccountInfoJabberImpl.java | 297 ++-- ...tionSetServerStoredAccountInfoMsnImpl.java | 29 + ...tionSetServerStoredAccountInfoSipImpl.java | 29 + .../accountinfo/AccountDetailsPanel.java | 1384 +++++++++++------ .../accountinfo/AccountInfoActivator.java | 78 +- .../AccountInfoMenuItemComponent.java | 119 ++ .../plugin/accountinfo/AccountInfoPanel.java | 246 ++- .../accountinfo/accountinfo.manifest.mf | 24 +- .../desktoputil/DesktopUtilActivator.java | 77 + .../plugin/desktoputil/FramedImage.java | 42 +- .../desktoputil/FramedImageWithMenu.java | 18 +- .../desktoputil/desktoputil.manifest.mf | 2 + .../presence/avatar/AvatarStackManager.java | 34 +- .../presence/avatar/SelectAvatarMenu.java | 118 +- .../avatar/imagepicker/EditPanel.java | 13 +- .../avatar/imagepicker/ImageClipper.java | 2 +- .../avatar/imagepicker/ImagePickerDialog.java | 20 +- .../avatar/imagepicker/WebcamDialog.java | 18 +- .../plugin/spellcheck/LanguageMenuBar.java | 5 + .../service/gui/AbstractPluginComponent.java | 7 + .../communicator/service/gui/Container.java | 6 + .../service/gui/PluginComponent.java | 15 + .../protocol/AbstractOperationSetAvatar.java | 1 + .../OperationSetServerStoredAccountInfo.java | 37 +- .../service/protocol/ServerStoredDetails.java | 151 ++ 40 files changed, 2409 insertions(+), 771 deletions(-) create mode 100644 lib/installer-exclude/jcalendar-1.4.jar create mode 100644 resources/images/impl/gui/buttons/accountEditIcon.png create mode 100644 src/net/java/sip/communicator/impl/gui/main/account/AccountRightButtonMenu.java create mode 100644 src/net/java/sip/communicator/plugin/accountinfo/AccountInfoMenuItemComponent.java rename src/net/java/sip/communicator/{impl/gui/main => plugin/desktoputil}/presence/avatar/AvatarStackManager.java (82%) rename src/net/java/sip/communicator/{impl/gui/main => plugin/desktoputil}/presence/avatar/SelectAvatarMenu.java (67%) rename src/net/java/sip/communicator/{impl/gui/main => plugin/desktoputil}/presence/avatar/imagepicker/EditPanel.java (94%) rename src/net/java/sip/communicator/{impl/gui/main => plugin/desktoputil}/presence/avatar/imagepicker/ImageClipper.java (99%) rename src/net/java/sip/communicator/{impl/gui/main => plugin/desktoputil}/presence/avatar/imagepicker/ImagePickerDialog.java (90%) rename src/net/java/sip/communicator/{impl/gui/main => plugin/desktoputil}/presence/avatar/imagepicker/WebcamDialog.java (93%) diff --git a/.classpath b/.classpath index 9a9bf4cae..fb0eda4a2 100755 --- a/.classpath +++ b/.classpath @@ -83,5 +83,6 @@ + diff --git a/build.xml b/build.xml index 3d14c5e0a..d513fa163 100644 --- a/build.xml +++ b/build.xml @@ -1079,8 +1079,8 @@ bundle-globalshortcut,bundle-plugin-msofficecomm,bundle-libjitsi, bundle-customcontactactions, bundle-phonenumbercontactsource, bundle-demuxcontactsource, bundle-muc, - bundle-desktoputil, - bundle-globaldisplaydetails,bundle-plugin-propertieseditor"/> + bundle-desktoputil,bundle-globaldisplaydetails, + bundle-plugin-propertieseditor,bundle-plugin-accountinfo"/> @@ -2226,6 +2226,7 @@ javax.swing.event, javax.swing.border"/> manifest="${src}/net/java/sip/communicator/plugin/accountinfo/accountinfo.manifest.mf"> + diff --git a/lib/felix.client.run.properties b/lib/felix.client.run.properties index cf8c6e429..f567442e7 100644 --- a/lib/felix.client.run.properties +++ b/lib/felix.client.run.properties @@ -134,6 +134,7 @@ felix.auto.start.60= \ felix.auto.start.66= \ reference:file:sc-bundles/swing-ui.jar \ reference:file:sc-bundles/update.jar \ + reference:file:sc-bundles/accountinfo.jar \ reference:file:sc-bundles/swingnotification.jar \ reference:file:sc-bundles/systray-service.jar \ reference:file:sc-bundles/osdependent.jar \ diff --git a/lib/installer-exclude/jcalendar-1.4.jar b/lib/installer-exclude/jcalendar-1.4.jar new file mode 100644 index 0000000000000000000000000000000000000000..8b0bff616278f2733e4c73daadcdce6f834e4572 GIT binary patch literal 169029 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 literal 0 HcmV?d00001 diff --git a/resources/images/images.properties b/resources/images/images.properties index b313f79ba..eed832ebf 100644 --- a/resources/images/images.properties +++ b/resources/images/images.properties @@ -123,6 +123,7 @@ service.gui.statusicons.USER_FFC_ICON=resources/images/impl/gui/common/statusico service.gui.statusicons.USER_ON_THE_PHONE_ICON=resources/images/impl/gui/common/statusicons/onThePhone.png # service gui buttons +service.gui.buttons.ACCOUNT_EDIT_ICON=resources/images/impl/gui/buttons/accountEditIcon.png service.gui.buttons.CONTACT_LIST_BUTTON_BG_LEFT=resources/images/impl/gui/buttons/contactListButtonBgLeft.png service.gui.buttons.CONTACT_LIST_BUTTON_BG_RIGHT=resources/images/impl/gui/buttons/contactListButtonBgRight.png service.gui.buttons.CONTACT_LIST_BUTTON_BG_MIDDLE=resources/images/impl/gui/buttons/contactListButtonBgMiddle.png diff --git a/resources/images/impl/gui/buttons/accountEditIcon.png b/resources/images/impl/gui/buttons/accountEditIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..a13ecc14fec96db4b8dcc8690819be25f8484ccb GIT binary patch literal 1559 zcmbVMeNfY87>~k4P@MIHiEJ(g=a||w=|^Y-o!Ta7)nX|?_Cc`%0{d0O zTs`#6sVuVvG;%x*YG9SzfuJaeVKA!JU>G_LR3fNSfk-zdN7c9n!~3`jLp|)Tv@X`t^ zEpR^GA=Seb9|B9cd%2;YqT~%epJ$|^PeQWg`{NTL$K zv?!`YNll6xBa|8)fss08h~q_AC8p96n8Bz@F_0)qs~oi5zd^xV%sYSlpb*32x2{8q+Z_!Avm zLOWAPD1zWK$;GsEOd91bec?JtATXcxb(B(6I5TaTZZJ+CM{bb_r`8mIktLeS$xSG zTN;WwGmgbhz71@BM0raiEp>NJb%aN!5_?^r@2|zcg(dHUJ&AeEt$D{6^bYED701V| zdGO?lu;l#Fs>coXu1_bKbXBI-$=_+KGQK{&b=M60gs0I+WZSX%?IVlQ4%#{@D=RMq z9+WMFA0uO;6 zJ0A#e)$VO`_RTxDb4)l@yg62bZolYkz25G7W#VQ>AMoQ{-G>o_-<175m1h%nUY^`^ z@1g#^?c5XCTA#VaSsK^HullO?zeUKVAKuTvEzN-u2dd^gy&soJbe_5iukM*za@p0iiGb>k zWX)^x6&-52mmInF`w^pd|GDABSh9BG+LIp@cLXlg@U=_3n!}1`4T^~=u;1-|GHe~u z7R{I1eL{_G%AxAFXM7e{EhLPZPh@lF8q3ea700QLn-=}OmRoCX>mH=97zCsy_P~Ex Q`QUFKFp;UmVS|0uKZv12vH$=8 literal 0 HcmV?d00001 diff --git a/resources/languages/resources.properties b/resources/languages/resources.properties index bee961c46..a41049430 100644 --- a/resources/languages/resources.properties +++ b/resources/languages/resources.properties @@ -857,15 +857,33 @@ impl.googlecontacts.WRONG_CREDENTIALS=Wrong credentials for Google account {0} plugin.accountinfo.TITLE=Account Info plugin.accountinfo.EXTENDED=Extended plugin.accountinfo.NOT_SUPPORTED=Account info not available. +plugin.accountinfo.SELECT_ACCOUNT=Please select an account: +plugin.accountinfo.DISPLAY_NAME=Display name: plugin.accountinfo.FIRST_NAME=First Name: plugin.accountinfo.MIDDLE_NAME=Middle Name: plugin.accountinfo.LAST_NAME=Last Name: +plugin.accountinfo.NICKNAME=Nickname: +plugin.accountinfo.URL=URL: plugin.accountinfo.AGE=Age: plugin.accountinfo.BDAY=Birth Date: +plugin.accountinfo.BDAY_FORMAT=MMM dd, yyyy plugin.accountinfo.GENDER=Gender: +plugin.accountinfo.STREET=Street Address: +plugin.accountinfo.CITY=City: +plugin.accountinfo.REGION=Region: +plugin.accountinfo.POST=Postal code: +plugin.accountinfo.COUNTRY=Country: plugin.accountinfo.EMAIL=E-mail: +plugin.accountinfo.WORK_EMAIL=Work E-mail: plugin.accountinfo.PHONE=Phone: +plugin.accountinfo.WORK_PHONE=Work Phone: +plugin.accountinfo.ORGANIZATION=Organization Name: +plugin.accountinfo.JOB_TITLE=Job Title: +plugin.accountinfo.ABOUT_ME=About Me: +plugin.accountinfo.ABOUT_ME_MAX_CHARACTERS=200 plugin.accountinfo.USER_PICTURES=User Pictures +plugin.accountinfo.GLOBAL_ICON=Use global icon +plugin.accountinfo.LOCAL_ICON=Use this icon: plugin.accountinfo.CHANGE=Change plugin.accountinfo.ONLY_MESSAGE=Only messages diff --git a/src/net/java/sip/communicator/impl/gui/main/MainFrame.java b/src/net/java/sip/communicator/impl/gui/main/MainFrame.java index 2313cdbea..a0fd2aeb6 100644 --- a/src/net/java/sip/communicator/impl/gui/main/MainFrame.java +++ b/src/net/java/sip/communicator/impl/gui/main/MainFrame.java @@ -186,7 +186,7 @@ public MainFrame() this.contactListPanel = new ContactListPane(this); - this.accountStatusPanel = new AccountStatusPanel(this); + this.accountStatusPanel = new AccountStatusPanel(); this.searchField = new SearchField( this, TreeContactList.searchFilter, diff --git a/src/net/java/sip/communicator/impl/gui/main/account/AccountList.java b/src/net/java/sip/communicator/impl/gui/main/account/AccountList.java index ae0a105a1..c2cc0ae4f 100644 --- a/src/net/java/sip/communicator/impl/gui/main/account/AccountList.java +++ b/src/net/java/sip/communicator/impl/gui/main/account/AccountList.java @@ -56,6 +56,12 @@ public class AccountList */ private final JButton editButton; + /** + * The menu that appears when right click on account is detected. + */ + private final AccountRightButtonMenu rightButtonMenu + = new AccountRightButtonMenu(); + /** * Creates an instance of this account list by specifying the parent * container of the list. @@ -69,6 +75,29 @@ public AccountList(AccountsConfigurationPanel parentConfigPanel) this.addMouseListener(this); + this.addMouseListener(new MouseAdapter() + { + public void mousePressed(MouseEvent e) + { + if (SwingUtilities.isRightMouseButton(e)) + { + Point point = e.getPoint(); + + AccountList.this.setSelectedIndex(getRow(point)); + + rightButtonMenu.setAccount(getSelectedAccount()); + + SwingUtilities.convertPointToScreen( + point, AccountList.this); + + ((JPopupMenu) rightButtonMenu).setInvoker(AccountList.this); + + rightButtonMenu.setLocation(point.x, point.y); + rightButtonMenu.setVisible(true); + } + } + }); + this.accountsInit(); GuiActivator.bundleContext.addServiceListener(this); @@ -76,6 +105,11 @@ public AccountList(AccountsConfigurationPanel parentConfigPanel) this.editButton = parentConfigPanel.getEditButton(); } + private int getRow(Point point) + { + return locationToIndex(point); + } + /** * Initializes the accounts table. */ diff --git a/src/net/java/sip/communicator/impl/gui/main/account/AccountRightButtonMenu.java b/src/net/java/sip/communicator/impl/gui/main/account/AccountRightButtonMenu.java new file mode 100644 index 000000000..f74ffb9c5 --- /dev/null +++ b/src/net/java/sip/communicator/impl/gui/main/account/AccountRightButtonMenu.java @@ -0,0 +1,260 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.gui.main.account; + +import java.awt.*; +import java.awt.event.*; +import java.util.*; + +import javax.swing.*; + +import net.java.sip.communicator.impl.gui.*; +import net.java.sip.communicator.impl.gui.event.*; +import net.java.sip.communicator.impl.gui.utils.*; +import net.java.sip.communicator.plugin.desktoputil.*; +import net.java.sip.communicator.service.gui.*; +import net.java.sip.communicator.service.gui.Container; +import net.java.sip.communicator.util.*; +import net.java.sip.communicator.util.skin.*; + +import org.jitsi.service.resources.*; +import org.osgi.framework.*; + +/** + * AccountRightButtonMenu is the menu that opens when user right clicks on any + * of his accounts in the account list section. + * + * @author Marin Dzhigarov + */ +public class AccountRightButtonMenu + extends SIPCommPopupMenu + implements ActionListener, + PluginComponentListener, + Skinnable +{ + /** + * The serial version UID. + */ + private static final long serialVersionUID = 1L; + + /** + * The logger of this class. + */ + private final Logger logger + = Logger.getLogger(AccountRightButtonMenu.class); + + /** + * The Account that is clicked on. + */ + private Account account = null; + + /** + * A list of all PluginComponents that are registered through the OSGi + * bundle context. + */ + private java.util.List pluginComponents + = new ArrayList(); + + /** + * The edit item. + */ + private final JMenuItem editItem = new JMenuItem( + GuiActivator.getResources().getI18NString( + "service.gui.EDIT")); + + /** + * Creates an instance of AccountRightButtonMenu + */ + public AccountRightButtonMenu() + { + super(); + + this.setLocation(getLocation()); + + this.init(); + + loadSkin(); + } + + /** + * Sets the current Account that is clicked on. + * @param account the Account that is clicked on. + */ + public void setAccount(Account account) + { + this.account = account; + editItem.setEnabled(account != null && this.account.isEnabled()); + for (PluginComponent pluginComponent : pluginComponents) + pluginComponent.setCurrentAccountID(account.getAccountID()); + } + + /** + * Returns the Account that was last clicked on. + * @return the Account that was last clicked on. + */ + public Account getAccount() + { + return account; + } + + /** + * Initialized the menu by adding all containing menu items. + */ + private void init() + { + initPluginComponents(); + add(editItem); + editItem.addActionListener(this); + } + + /** + * Initializes plug-in components for this container. + */ + private void initPluginComponents() + { + // Search for plugin components registered through the OSGI bundle + // context. + ServiceReference[] serRefs = null; + + String osgiFilter = "(" + + Container.CONTAINER_ID + + "="+Container.CONTAINER_ACCOUNT_RIGHT_BUTTON_MENU.getID()+")"; + + try + { + serRefs = GuiActivator.bundleContext.getServiceReferences( + PluginComponentFactory.class.getName(), + osgiFilter); + } + catch (InvalidSyntaxException exc) + { + logger.error("Could not obtain plugin reference.", exc); + } + + if (serRefs != null) + { + for (int i = 0; i < serRefs.length; i ++) + { + PluginComponentFactory factory = + (PluginComponentFactory) GuiActivator + .bundleContext.getService(serRefs[i]); + + PluginComponent component = + factory.getPluginComponentInstance(this); + + if (component.getComponent() == null) + continue; + + pluginComponents.add(component); + + if (factory.getPositionIndex() != -1) + this.add((Component)component.getComponent(), + factory.getPositionIndex()); + else + this.add((Component)component.getComponent()); + } + } + GuiActivator.getUIService().addPluginComponentListener(this); + } + + /** + * Reloads skin related information. + */ + public void loadSkin() + { + editItem.setIcon(new ImageIcon( + ImageLoader.getImage(ImageLoader.ACCOUNT_EDIT_ICON))); + } + + /** + * Indicates that a plugin component has been added to this container. + * + * @param event the PluginComponentEvent that notified us + */ + /** + * Indicates that a new plugin component has been added. Adds it to this + * container if it belongs to it. + * + * @param event the PluginComponentEvent that notified us + */ + public void pluginComponentAdded(PluginComponentEvent event) + { + PluginComponentFactory factory = event.getPluginComponentFactory(); + + if (!factory.getContainer().equals( + Container.CONTAINER_ACCOUNT_RIGHT_BUTTON_MENU)) + return; + + PluginComponent c = factory.getPluginComponentInstance(this); + + this.add((Component) c.getComponent()); + + this.repaint(); + } + + /** + * Removes the according plug-in component from this container. + * @param event the PluginComponentEvent that notified us + */ + public void pluginComponentRemoved(PluginComponentEvent event) + { + PluginComponentFactory factory = event.getPluginComponentFactory(); + + if(factory.getContainer() + .equals(Container.CONTAINER_ACCOUNT_RIGHT_BUTTON_MENU)) + { + Component c = + (Component)factory.getPluginComponentInstance(this) + .getComponent(); + this.remove(c); + pluginComponents.remove(c); + } + } + + /** + * Handles the ActionEvent. Determines which menu item was + * selected and performs the appropriate operations. + * @param e the ActionEvent, which notified us of the action + */ + public void actionPerformed(ActionEvent e) + { + JMenuItem menuItem = (JMenuItem) e.getSource(); + + if (menuItem.equals(editItem)) + { + if (account == null) + return; + + AccountRegWizardContainerImpl wizard = + (AccountRegWizardContainerImpl) GuiActivator.getUIService() + .getAccountRegWizardContainer(); + + AccountRegistrationWizard protocolWizard = + wizard.getProtocolWizard(account.getProtocolProvider()); + + ResourceManagementService resources = GuiActivator.getResources(); + if (protocolWizard != null) + { + wizard.setTitle(resources.getI18NString( + "service.gui.ACCOUNT_REGISTRATION_WIZARD")); + + wizard.modifyAccount(account.getProtocolProvider()); + wizard.showDialog(false); + } + else + { + // There is no wizard for this account - just show an error + // dialog: + String title = resources.getI18NString("service.gui.ERROR"); + String message = + resources.getI18NString("service.gui.EDIT_NOT_SUPPORTED"); + ErrorDialog dialog = new ErrorDialog(null, title, message); + dialog.setVisible(true); + } + } + } +} diff --git a/src/net/java/sip/communicator/impl/gui/main/presence/AccountStatusPanel.java b/src/net/java/sip/communicator/impl/gui/main/presence/AccountStatusPanel.java index 9b39b5dff..5e7d615f8 100644 --- a/src/net/java/sip/communicator/impl/gui/main/presence/AccountStatusPanel.java +++ b/src/net/java/sip/communicator/impl/gui/main/presence/AccountStatusPanel.java @@ -14,9 +14,9 @@ import net.java.sip.communicator.impl.gui.*; import net.java.sip.communicator.impl.gui.event.*; import net.java.sip.communicator.impl.gui.lookandfeel.*; -import net.java.sip.communicator.impl.gui.main.*; -import net.java.sip.communicator.impl.gui.main.presence.avatar.*; import net.java.sip.communicator.impl.gui.utils.*; +import net.java.sip.communicator.plugin.desktoputil.*; +import net.java.sip.communicator.plugin.desktoputil.presence.avatar.*; import net.java.sip.communicator.service.globaldisplaydetails.event.*; import net.java.sip.communicator.service.gui.*; import net.java.sip.communicator.service.gui.Container; @@ -25,8 +25,6 @@ import net.java.sip.communicator.util.*; import net.java.sip.communicator.util.skin.*; -import net.java.sip.communicator.plugin.desktoputil.*; - import org.jitsi.util.*; /** @@ -126,15 +124,13 @@ public class AccountStatusPanel /** * Creates an instance of AccountStatusPanel by specifying the * main window, where this panel is added. - * @param mainFrame the main window, where this panel is added */ - public AccountStatusPanel(MainFrame mainFrame) + public AccountStatusPanel() { super(new BorderLayout(10, 0)); FramedImageWithMenu imageWithMenu = new FramedImageWithMenu( - mainFrame, new ImageIcon( ImageLoader .getImage(ImageLoader.DEFAULT_USER_PHOTO)), diff --git a/src/net/java/sip/communicator/impl/gui/swing.ui.manifest.mf b/src/net/java/sip/communicator/impl/gui/swing.ui.manifest.mf index ce04afbe4..607dffc6a 100644 --- a/src/net/java/sip/communicator/impl/gui/swing.ui.manifest.mf +++ b/src/net/java/sip/communicator/impl/gui/swing.ui.manifest.mf @@ -58,6 +58,7 @@ Import-Package: com.apple.eawt, net.java.sip.communicator.plugin.desktoputil.event, net.java.sip.communicator.plugin.desktoputil.plaf, net.java.sip.communicator.plugin.desktoputil.presence, + net.java.sip.communicator.plugin.desktoputil.presence.avatar, net.java.sip.communicator.plugin.desktoputil.transparent, net.java.sip.communicator.service.customcontactactions, net.java.sip.communicator.service.globaldisplaydetails, diff --git a/src/net/java/sip/communicator/impl/gui/utils/ImageLoader.java b/src/net/java/sip/communicator/impl/gui/utils/ImageLoader.java index 083e2d1c8..c2dba5779 100644 --- a/src/net/java/sip/communicator/impl/gui/utils/ImageLoader.java +++ b/src/net/java/sip/communicator/impl/gui/utils/ImageLoader.java @@ -160,6 +160,12 @@ public class ImageLoader public static final ImageID RIGHT_ARROW_ICON = new ImageID("service.gui.icons.RIGHT_ARROW_ICON"); + /** + * The edit icon that is shown when account is right clicked on. + */ + public static final ImageID ACCOUNT_EDIT_ICON + = new ImageID("service.gui.buttons.ACCOUNT_EDIT_ICON"); + /** * The call button image. */ diff --git a/src/net/java/sip/communicator/impl/protocol/icq/OperationSetServerStoredAccountInfoIcqImpl.java b/src/net/java/sip/communicator/impl/protocol/icq/OperationSetServerStoredAccountInfoIcqImpl.java index 19c099244..0d10453ca 100644 --- a/src/net/java/sip/communicator/impl/protocol/icq/OperationSetServerStoredAccountInfoIcqImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/icq/OperationSetServerStoredAccountInfoIcqImpl.java @@ -52,6 +52,7 @@ public class OperationSetServerStoredAccountInfoIcqImpl public static final Map, int[]> supportedTypes = new Hashtable, int[]>(); static { + supportedTypes.put(ServerStoredDetails.ImageDetail.class, new int[]{1}); supportedTypes.put(ServerStoredDetails.CountryDetail.class, new int[]{1, 0x01A4}); supportedTypes.put(ServerStoredDetails.NicknameDetail.class, new int[]{1, 0x0154}); supportedTypes.put(ServerStoredDetails.FirstNameDetail.class, new int[]{1, 0x0140}); @@ -226,6 +227,23 @@ public boolean isDetailClassSupported( return supportedTypes.get(detailClass) != null; } + /** + * Determines whether the underlying implementation supports edition + * of this detail class. + *

+ * @param detailClass the class whose edition we'd like to determine if it's + * possible + * @return true if the underlying implementation supports edition of this + * type of detail and false otherwise. + */ + public boolean isDetailClassEditable( + Class detailClass) + { + return + isDetailClassSupported(detailClass) + && ImageDetail.class.isAssignableFrom(detailClass); + } + /** * Utility method throwing an exception if the icq stack is not properly * initialized. @@ -794,6 +812,18 @@ else if(newDetailValue.equals(ServerStoredDetails.GenderDetail.MALE)) return false; } + /* + * (non-Javadoc) + * @see net.java.sip.communicator.service.protocol.OperationSetServerStoredAccountInfo#save() + * This method is currently unimplemented. + * The idea behind this method is for users to call it only once, meaning + * that all ServerStoredDetails previously modified by addDetail/removeDetail + * and/or replaceDetail will be saved online on the server in one step. + * Currently, addDetail/removeDetail/replaceDetail methods are doing the + * actual saving but in the future the saving part must be carried here. + */ + public void save() throws OperationFailedException {} + /** * Requests the account image if its missing. * @return the new image or the one that has been already downloaded. diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/InfoRetreiver.java b/src/net/java/sip/communicator/impl/protocol/jabber/InfoRetreiver.java index af1be57e1..869e51d82 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/InfoRetreiver.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/InfoRetreiver.java @@ -8,6 +8,7 @@ import java.lang.reflect.*; import java.net.*; +import java.text.*; import java.util.*; import net.java.sip.communicator.service.protocol.ServerStoredDetails.*; @@ -175,6 +176,24 @@ protected List retrieveDetails(String contactAddress) if(tmp != null) result.add(new NicknameDetail(tmp)); + tmp = card.getField("BDAY"); + if (tmp != null) + { + try + { + Calendar birthDateCalendar = Calendar.getInstance(); + DateFormat dateFormat = + new SimpleDateFormat( + JabberActivator.getResources().getI18NString( + "plugin.accountinfo.BDAY_FORMAT")); + Date birthDate = + dateFormat.parse(tmp); + birthDateCalendar.setTime(birthDate); + BirthDateDetail bd = new BirthDateDetail(birthDateCalendar); + result.add(bd); + } + catch (ParseException e) {} + } // Home Details // addrField one of // POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET, @@ -195,9 +214,9 @@ protected List retrieveDetails(String contactAddress) if(tmp != null) result.add(new PostalCodeDetail(tmp)); -// tmp = card.getAddressFieldHome("CTRY"); -// if(tmp != null) -// result.add(new CountryDetail(tmp); + tmp = card.getAddressFieldHome("CTRY"); + if(tmp != null) + result.add(new CountryDetail(tmp)); // phoneType one of //VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF @@ -276,7 +295,7 @@ protected List retrieveDetails(String contactAddress) tmp = card.getEmailWork(); if(tmp != null) - result.add(new EmailAddressDetail(tmp)); + result.add(new WorkEmailAddressDetail(tmp)); tmp = card.getOrganization(); if(tmp != null) @@ -286,15 +305,25 @@ protected List retrieveDetails(String contactAddress) if(tmp != null) result.add(new WorkDepartmentNameDetail(tmp)); + tmp = card.getField("TITLE"); + if(tmp != null) + result.add(new JobTitleDetail(tmp)); + + tmp = card.getField("ABOUTME"); + if (tmp != null) + result.add(new AboutMeDetail(tmp)); + byte[] imageBytes = card.getAvatar(); if(imageBytes != null && imageBytes.length > 0) + { result.add(new ImageDetail("Image", imageBytes)); + } try { tmp = card.getField("URL"); if(tmp != null) - result.add(new WebPageDetail(new URL(tmp))); + result.add(new URLDetail("URL", new URL(tmp))); } catch(MalformedURLException e){} } diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetServerStoredAccountInfoJabberImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetServerStoredAccountInfoJabberImpl.java index 9d148e8d3..09b9e5f11 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetServerStoredAccountInfoJabberImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetServerStoredAccountInfoJabberImpl.java @@ -6,11 +6,12 @@ */ package net.java.sip.communicator.impl.protocol.jabber; +import java.net.*; +import java.text.*; import java.util.*; import net.java.sip.communicator.service.protocol.*; -import net.java.sip.communicator.service.protocol.ServerStoredDetails.GenericDetail; -import net.java.sip.communicator.service.protocol.ServerStoredDetails.ImageDetail; +import net.java.sip.communicator.service.protocol.ServerStoredDetails.*; import net.java.sip.communicator.service.protocol.event.*; import net.java.sip.communicator.util.*; @@ -22,6 +23,7 @@ * provider. * * @author Damian Minkov + * @author Marin Dzhigarov */ public class OperationSetServerStoredAccountInfoJabberImpl extends AbstractOperationSetServerStoredAccountInfo @@ -42,6 +44,35 @@ public class OperationSetServerStoredAccountInfoJabberImpl */ private ProtocolProviderServiceJabberImpl jabberProvider = null; + /** + * List of all supported ServerStoredDetails + * for this implementation. + */ + public static final List> supportedTypes + = new ArrayList>(); + + static { + supportedTypes.add(ImageDetail.class); + supportedTypes.add(FirstNameDetail.class); + supportedTypes.add(MiddleNameDetail.class); + supportedTypes.add(LastNameDetail.class); + supportedTypes.add(NicknameDetail.class); + supportedTypes.add(AddressDetail.class); + supportedTypes.add(CityDetail.class); + supportedTypes.add(ProvinceDetail.class); + supportedTypes.add(PostalCodeDetail.class); + supportedTypes.add(CountryDetail.class); + supportedTypes.add(EmailAddressDetail.class); + supportedTypes.add(WorkEmailAddressDetail.class); + supportedTypes.add(PhoneNumberDetail.class); + supportedTypes.add(WorkPhoneDetail.class); + supportedTypes.add(WorkOrganizationNameDetail.class); + supportedTypes.add(URLDetail.class); + supportedTypes.add(BirthDateDetail.class); + supportedTypes.add(JobTitleDetail.class); + supportedTypes.add(AboutMeDetail.class); + } + /** * Our account UIN. */ @@ -124,14 +155,7 @@ public Iterator getAllAvailableDetails() */ public Iterator> getSupportedDetailTypes() { - List details = infoRetreiver.getContactDetails(uin); - List> result - = new Vector>(); - - for (GenericDetail obj : details) - result.add(obj.getClass()); - - return result.iterator(); + return supportedTypes.iterator(); } /** @@ -150,12 +174,7 @@ public Iterator> getSupportedDetailTypes() public boolean isDetailClassSupported( Class detailClass) { - List details = infoRetreiver.getContactDetails(uin); - - for (GenericDetail obj : details) - if(detailClass.isAssignableFrom(obj.getClass())) - return true; - return false; + return supportedTypes.contains(detailClass); } /** @@ -173,7 +192,7 @@ public int getMaxDetailInstances(Class detailClass) } /** - * Adds the specified detail to the list of details registered on-line + * Adds the specified detail to the list of details ready to be saved online * for this account. If such a detail already exists its max instance number * is consulted and if it allows it - a second instance is added or otherwise * and illegal argument exception is thrown. An IllegalArgumentException is @@ -187,67 +206,50 @@ public int getMaxDetailInstances(Class detailClass) * max instances number has been attained or if the underlying * implementation does not support setting details of the corresponding * class. - * @throws OperationFailedException with code Network Failure if putting the - * new value online has failed * @throws java.lang.ArrayIndexOutOfBoundsException if the number of * instances currently registered by the application is already equal to the * maximum number of supported instances (@see getMaxDetailInstances()) */ public void addDetail(ServerStoredDetails.GenericDetail detail) throws IllegalArgumentException, - OperationFailedException, ArrayIndexOutOfBoundsException { - assertConnected(); - - /* - Currently as the function only provided the list of classes that - currently have data associated with them - in Jabber InfoRetreiver we have to skip this check*/ - //if (!isDetailClassSupported(detail.getClass())) { - // throw new IllegalArgumentException( - // "implementation does not support such details " + - // detail.getClass()); - //} + if (!isDetailClassSupported(detail.getClass())) { + throw new IllegalArgumentException( + "implementation does not support such details " + + detail.getClass()); + } Iterator iter = getDetails(detail.getClass()); int currentDetailsSize = 0; while (iter.hasNext()) { currentDetailsSize++; + iter.next(); } - if (currentDetailsSize >= getMaxDetailInstances(detail.getClass())) + if (currentDetailsSize > getMaxDetailInstances(detail.getClass())) { throw new ArrayIndexOutOfBoundsException( "Max count for this detail is already reached"); } - if(detail instanceof ImageDetail) - { - // Push the avatar photo to the server. - this.uploadImageDetail( - ServerStoredDetailsChangeEvent.DETAIL_ADDED, - null, - detail); - } + infoRetreiver.getCachedContactDetails(uin).add(detail); } /** - * Removes the specified detail from the list of details stored online for - * this account. The method returns a boolean indicating if such a detail - * was found (and removed) or not. + * Removes the specified detail from the list of details ready to be saved + * online this account. The method returns a boolean indicating if such a + * detail was found (and removed) or not. *

* @param detail the detail to remove * @return true if the specified detail existed and was successfully removed * and false otherwise. - * @throws OperationFailedException with code Network Failure if removing the - * detail from the server has failed */ public boolean removeDetail(ServerStoredDetails.GenericDetail detail) - throws OperationFailedException { - return false; + + return infoRetreiver.getCachedContactDetails(uin).remove(detail); } /** @@ -264,16 +266,12 @@ public boolean removeDetail(ServerStoredDetails.GenericDetail detail) * call to addDetail is required). * @throws ClassCastException if newDetailValue is not an instance of the * same class as currentDetailValue. - * @throws OperationFailedException with code Network Failure if putting the - * new value back online has failed */ public boolean replaceDetail( ServerStoredDetails.GenericDetail currentDetailValue, ServerStoredDetails.GenericDetail newDetailValue) - throws ClassCastException, OperationFailedException + throws ClassCastException { - assertConnected(); - if (!newDetailValue.getClass().equals(currentDetailValue.getClass())) { throw new ClassCastException( @@ -304,22 +302,130 @@ public boolean replaceDetail( return false; } - if(newDetailValue instanceof ImageDetail) + removeDetail(currentDetailValue); + addDetail(newDetailValue); + return true; + } + + /** + * Saves the list of details for this account that were ready to be stored + * online on the server. This method performs the actual saving of details + * online on the server and is supposed to be invoked after addDetail(), + * replaceDetail() and/or removeDetail(). + *

+ * @throws OperationFailedException with code Network Failure if putting the + * new values back online has failed. + */ + public void save() throws OperationFailedException + { + assertConnected(); + + List details = infoRetreiver.getContactDetails(uin); + VCardXEP0153 vCard = new VCardXEP0153(); + for (GenericDetail detail : details) + { + if (detail instanceof ImageDetail) + { + byte[] avatar = ((ImageDetail) detail).getBytes(); + if (avatar == null) vCard.setAvatar(new byte[0]); + else vCard.setAvatar(avatar); + fireServerStoredDetailsChangeEvent( + jabberProvider, + ServerStoredDetailsChangeEvent.DETAIL_ADDED, + null, + detail); + } + else if (detail.getClass().equals(FirstNameDetail.class)) + vCard.setFirstName((String)detail.getDetailValue()); + else if (detail.getClass().equals(MiddleNameDetail.class)) + vCard.setMiddleName((String)detail.getDetailValue()); + else if (detail.getClass().equals(LastNameDetail.class)) + vCard.setLastName((String)detail.getDetailValue()); + else if (detail.getClass().equals(NicknameDetail.class)) + vCard.setNickName((String)detail.getDetailValue()); + else if (detail.getClass().equals(URLDetail.class)) + { + if (detail.getDetailValue() != null) + vCard.setField( + "URL", ((URL)detail.getDetailValue()).toString()); + } + else if (detail.getClass().equals(BirthDateDetail.class)) + { + if (detail.getDetailValue() != null) + { + Calendar c = ((BirthDateDetail)detail).getCalendar(); + DateFormat dateFormat = + new SimpleDateFormat( + JabberActivator.getResources().getI18NString( + "plugin.accountinfo.BDAY_FORMAT")); + String strdate = dateFormat.format(c.getTime()); + vCard.setField("BDAY", strdate); + } + } + else if (detail.getClass().equals(AddressDetail.class)) + vCard.setAddressFieldHome( + "STREET", (String)detail.getDetailValue()); + else if (detail.getClass().equals(CityDetail.class)) + vCard.setAddressFieldHome( + "LOCALITY", (String)detail.getDetailValue()); + else if (detail.getClass().equals(ProvinceDetail.class)) + vCard.setAddressFieldHome( + "REGION", (String)detail.getDetailValue()); + else if (detail.getClass().equals(PostalCodeDetail.class)) + vCard.setAddressFieldHome( + "PCODE", (String)detail.getDetailValue()); + else if (detail.getClass().equals(CountryDetail.class)) + vCard.setAddressFieldHome( + "CTRY", (String)detail.getDetailValue()); + else if (detail.getClass().equals(PhoneNumberDetail.class)) + vCard.setPhoneHome("VOICE", (String)detail.getDetailValue()); + else if (detail.getClass().equals(WorkPhoneDetail.class)) + vCard.setPhoneWork("VOICE", (String)detail.getDetailValue()); + else if (detail.getClass().equals(EmailAddressDetail.class)) + vCard.setEmailHome((String)detail.getDetailValue()); + else if (detail.getClass().equals(WorkEmailAddressDetail.class)) + vCard.setEmailWork((String)detail.getDetailValue()); + else if (detail.getClass().equals(WorkOrganizationNameDetail.class)) + vCard.setOrganization((String)detail.getDetailValue()); + else if (detail.getClass().equals(JobTitleDetail.class)) + vCard.setField("TITLE", (String)detail.getDetailValue()); + else if (detail.getClass().equals(AboutMeDetail.class)) + vCard.setField("ABOUTME", (String)detail.getDetailValue()); + } + try { - // Push the new avatar photo to the server. - return this.uploadImageDetail( - ServerStoredDetailsChangeEvent.DETAIL_REPLACED, - currentDetailValue, - newDetailValue); + vCard.save(jabberProvider.getConnection()); } + catch (XMPPException xmppe) + { + logger.error("Error loading/saving vcard: ", xmppe); + throw new OperationFailedException( + "Error loading/saving vcard: ", 1, xmppe); + } + } + /** + * Determines whether the underlying implementation supports edition + * of this detail class. + *

+ * @param detailClass the class whose edition we'd like to determine if it's + * possible + * @return true if the underlying implementation supports edition of this + * type of detail and false otherwise. + */ + public boolean isDetailClassEditable( + Class detailClass) + { + if (isDetailClassSupported(detailClass)) { + return true; + } return false; } /** - * Utility method throwing an exception if the icq stack is not properly + * Utility method throwing an exception if the jabber stack is not properly * initialized. - * @throws java.lang.IllegalStateException if the underlying ICQ stack is + * @throws java.lang.IllegalStateException if the underlying jabber stack is * not registered and initialized. */ private void assertConnected() throws IllegalStateException @@ -333,73 +439,4 @@ private void assertConnected() throws IllegalStateException "The jabber provider must be signed on before " +"being able to communicate."); } - - /** - * Uploads the new avatar image to the server via the vCard mechanism - * (XEP-0153). - * - * @param changeEventID the int ID of the event to dispatch - * @param currentDetailValue the detail value we'd like to replace. - * @param newDetailValue the value of the detail that we'd like to replace - * currentDetailValue with. If ((ImageDetail) newDetailValue).getBytes() is - * null, then this function removes the current avatar from the server by - * sending a vCard with a "photo" tag without any content. - * - * @return "true" if the new avatar image has been uploaded (even if the - * current avatar image is removed). "false" if an XMPPException occurs. - */ - private boolean uploadImageDetail( - int changeEventID, - ServerStoredDetails.GenericDetail currentDetailValue, - ServerStoredDetails.GenericDetail newDetailValue) - { - boolean isPhotoChanged = false; - - try - { - byte[] newAvatar = ((ImageDetail) newDetailValue).getBytes(); - - VCardXEP0153 v1 = new VCardXEP0153(); - // Retrieve the old vCard. - v1.load(jabberProvider.getConnection()); - // Checks if the new avatar photo is diferent form the server one. - // If yes, then upload the new avatar photo. - if(!Arrays.equals(v1.getAvatar(), newAvatar)) - { - if(newAvatar == null) - { - v1.setAvatar(new byte[0]); - } - else - { - v1.setAvatar(newAvatar); - } - // Saves the new vCard. - v1.save(jabberProvider.getConnection()); - } - - // Sets the new avatar photo advertised in all presence messages, - // and send one presence messge immediately. - ((OperationSetPersistentPresenceJabberImpl) - this.jabberProvider.getOperationSet( - OperationSetPersistentPresence.class)) - .updateAccountPhotoPresenceExtension(newAvatar); - - // Advertises all detail change listeners, that the server stored - // details have changed. - fireServerStoredDetailsChangeEvent( - jabberProvider, - changeEventID, - currentDetailValue, - newDetailValue); - - isPhotoChanged = true; - } - catch (XMPPException xmppe) - { - logger.error("Error loading/saving vcard: ", xmppe); - } - - return isPhotoChanged; - } -} +} \ No newline at end of file diff --git a/src/net/java/sip/communicator/impl/protocol/msn/OperationSetServerStoredAccountInfoMsnImpl.java b/src/net/java/sip/communicator/impl/protocol/msn/OperationSetServerStoredAccountInfoMsnImpl.java index 81aa89a4c..fb43723c5 100644 --- a/src/net/java/sip/communicator/impl/protocol/msn/OperationSetServerStoredAccountInfoMsnImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/msn/OperationSetServerStoredAccountInfoMsnImpl.java @@ -267,6 +267,23 @@ public boolean isDetailClassSupported( return false; } + /** + * Determines whether the underlying implementation supports edition + * of this detail class. + *

+ * @param detailClass the class whose edition we'd like to determine if it's + * possible + * @return true if the underlying implementation supports edition of this + * type of detail and false otherwise. + */ + public boolean isDetailClassEditable( + Class detailClass) + { + return + isDetailClassSupported(detailClass) + && ImageDetail.class.isAssignableFrom(detailClass); + } + /** * The method returns the number of instances supported for a particular * detail type. Some protocols offer storing multiple values for a @@ -518,6 +535,18 @@ public boolean replaceDetail( return false; } + /* + * (non-Javadoc) + * @see net.java.sip.communicator.service.protocol.OperationSetServerStoredAccountInfo#save() + * This method is currently unimplemented. + * The idea behind this method is for users to call it only once, meaning + * that all ServerStoredDetails previously modified by addDetail/removeDetail + * and/or replaceDetail will be saved online on the server in one step. + * Currently, addDetail/removeDetail/replaceDetail methods are doing the + * actual saving but in the future the saving part must be carried here. + */ + public void save() throws OperationFailedException {} + /** * Utility method throwing an exception if the icq stack is not properly * initialized. diff --git a/src/net/java/sip/communicator/impl/protocol/sip/OperationSetServerStoredAccountInfoSipImpl.java b/src/net/java/sip/communicator/impl/protocol/sip/OperationSetServerStoredAccountInfoSipImpl.java index 4ad22a72f..6b528aee4 100644 --- a/src/net/java/sip/communicator/impl/protocol/sip/OperationSetServerStoredAccountInfoSipImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/sip/OperationSetServerStoredAccountInfoSipImpl.java @@ -194,6 +194,23 @@ && isImageDetailSupported()) || DisplayNameDetail.class.isAssignableFrom(detailClass); } + /** + * Determines whether the underlying implementation supports the edition + * of this detail class. + *

+ * @param detailClass the class whose edition we'd like to determine if it's + * possible + * @return true if the underlying implementation supports edition of this + * type of detail and false otherwise. + */ + public boolean isDetailClassEditable( + Class detailClass) + { + return + isDetailClassSupported(detailClass) + && ImageDetail.class.isAssignableFrom(detailClass); + } + /** * The method returns the number of instances supported for a particular * detail type. @@ -411,6 +428,18 @@ public boolean replaceDetail( return true; } + /* + * (non-Javadoc) + * @see net.java.sip.communicator.service.protocol.OperationSetServerStoredAccountInfo#save() + * This method is currently unimplemented. + * The idea behind this method is for users to call it only once, meaning + * that all ServerStoredDetails previously modified by addDetail/removeDetail + * and/or replaceDetail will be saved online on the server in one step. + * Currently, addDetail/removeDetail/replaceDetail methods are doing the + * actual saving but in the future the saving part must be carried here. + */ + public void save() throws OperationFailedException {} + /** * Determines if image details is supported. * diff --git a/src/net/java/sip/communicator/plugin/accountinfo/AccountDetailsPanel.java b/src/net/java/sip/communicator/plugin/accountinfo/AccountDetailsPanel.java index 7ddf425e9..fa96f0367 100644 --- a/src/net/java/sip/communicator/plugin/accountinfo/AccountDetailsPanel.java +++ b/src/net/java/sip/communicator/plugin/accountinfo/AccountDetailsPanel.java @@ -7,75 +7,150 @@ import java.awt.*; import java.awt.event.*; -import java.io.*; -import java.text.*; +import java.beans.*; +import java.net.*; import java.util.*; -import javax.imageio.*; import javax.swing.*; +import javax.swing.text.*; + +import org.jitsi.util.*; import net.java.sip.communicator.plugin.desktoputil.*; +import net.java.sip.communicator.plugin.desktoputil.presence.avatar.*; +import net.java.sip.communicator.service.globaldisplaydetails.*; import net.java.sip.communicator.service.protocol.*; -import net.java.sip.communicator.service.protocol.ServerStoredDetails.BinaryDetail; +import net.java.sip.communicator.service.protocol.ServerStoredDetails.AboutMeDetail; +import net.java.sip.communicator.service.protocol.ServerStoredDetails.AddressDetail; import net.java.sip.communicator.service.protocol.ServerStoredDetails.BirthDateDetail; +import net.java.sip.communicator.service.protocol.ServerStoredDetails.CityDetail; +import net.java.sip.communicator.service.protocol.ServerStoredDetails.CountryDetail; +import net.java.sip.communicator.service.protocol.ServerStoredDetails.DisplayNameDetail; import net.java.sip.communicator.service.protocol.ServerStoredDetails.EmailAddressDetail; import net.java.sip.communicator.service.protocol.ServerStoredDetails.FirstNameDetail; import net.java.sip.communicator.service.protocol.ServerStoredDetails.GenderDetail; import net.java.sip.communicator.service.protocol.ServerStoredDetails.GenericDetail; +import net.java.sip.communicator.service.protocol.ServerStoredDetails.ImageDetail; +import net.java.sip.communicator.service.protocol.ServerStoredDetails.JobTitleDetail; import net.java.sip.communicator.service.protocol.ServerStoredDetails.LastNameDetail; import net.java.sip.communicator.service.protocol.ServerStoredDetails.MiddleNameDetail; +import net.java.sip.communicator.service.protocol.ServerStoredDetails.NicknameDetail; import net.java.sip.communicator.service.protocol.ServerStoredDetails.PhoneNumberDetail; -import net.java.sip.communicator.util.*; -import net.java.sip.communicator.util.skin.*; +import net.java.sip.communicator.service.protocol.ServerStoredDetails.PostalCodeDetail; +import net.java.sip.communicator.service.protocol.ServerStoredDetails.ProvinceDetail; +import net.java.sip.communicator.service.protocol.ServerStoredDetails.URLDetail; +import net.java.sip.communicator.service.protocol.ServerStoredDetails.WorkEmailAddressDetail; +import net.java.sip.communicator.service.protocol.ServerStoredDetails.WorkOrganizationNameDetail; +import net.java.sip.communicator.service.protocol.ServerStoredDetails.WorkPhoneDetail; + +import net.java.sip.communicator.util.Logger; + +import com.toedter.calendar.*; /** - * The right side panel of AccountDetailsDialog. Shows one tab of a summary of - * contact information for the selected subcontact, and has an extended tab - * listing all of the details. + * The main panel that allows users to view and edit their account information. + * Different instances of this class are created for every registered + * ProtocolProviderService. + * Currently, supported account details are first/middle/last names, nickname, + * street/city/region/country address, postal code, birth date, gender, + * organization name, job title, about me, home/work email, home/work phone. * * @author Yana Stamcheva * @author Adam Netocny + * @author Marin Dzhigarov */ public class AccountDetailsPanel extends TransparentPanel - implements Skinnable { - private static final long serialVersionUID = 5524135388175045624L; + /** + * Serial version UID. + */ + private static final long serialVersionUID = 1L; + /** + * The logger + */ private Logger logger = Logger.getLogger(AccountDetailsPanel.class); + /** + * Mapping between all supported by this plugin ServerStoredDetails + * and their respective JTextField that are used for modifying + * the details. + */ + private final Map, JTextField> + detailToTextField + = new HashMap, JTextField>(); + + /** + * The ProtocolProviderService that this panel is associated with. + */ + ProtocolProviderService protocolProvider; + /** * The operation set giving access to the server stored account details. */ private OperationSetServerStoredAccountInfo accountInfoOpSet; - private JTextField firstNameField = new JTextField(); + private JTextField displayNameField; + + private JTextField firstNameField; + + private JTextField middleNameField; + + private JTextField lastNameField; + + private JTextField nicknameField; + + private JTextField urlField; + + private JTextField streetAddressField; + + private JTextField cityField; - private JTextField middleNameField = new JTextField(); + private JTextField regionField; - private JTextField lastNameField = new JTextField(); + private JTextField postalCodeField; - private JTextField genderField = new JTextField(); + private JTextField countryField; - private JTextField ageField = new JTextField(); + private JTextField phoneField; - private JTextField birthdayField = new JTextField(); + private JTextField workPhoneField; - private JTextField emailField = new JTextField(); + private JTextField emailField; - private JTextField phoneField = new JTextField(); + private JTextField workEmailField; - private JLabel avatarLabel = new JLabel(); + private JTextField organizationField; + private JTextField jobTitleField; + + private JTextArea aboutMeArea; + + private JTextField genderField; + + private JTextField ageField; + + private JDateChooser birthDayCalendar; + + private JRadioButton globalIcon; + + private JRadioButton localIcon; + + private FramedImageWithMenu imageWithMenu; + /** + * The "apply" button. + */ private JButton applyButton = new JButton(Resources.getString("service.gui.APPLY")); + /** + * The panel containing all buttons. + */ private JPanel buttonPanel = new TransparentPanel(new FlowLayout(FlowLayout.CENTER)); - private JScrollPane mainScrollPane = new JScrollPane(); - - private boolean isDataLoaded = false; + private DisplayNameDetail displayNameDetail; private FirstNameDetail firstNameDetail; @@ -83,24 +158,47 @@ public class AccountDetailsPanel private LastNameDetail lastNameDetail; - private GenderDetail genderDetail; + private NicknameDetail nicknameDetail; - private BirthDateDetail birthDateDetail; + private URLDetail urlDetail; - private EmailAddressDetail emailDetail; + private AddressDetail streetAddressDetail; + + private CityDetail cityDetail; + + private ProvinceDetail regionDetail; + + private PostalCodeDetail postalCodeDetail; + + private CountryDetail countryDetail; private PhoneNumberDetail phoneDetail; + + private WorkPhoneDetail workPhoneDetail; + + private EmailAddressDetail emailDetail; + + private WorkEmailAddressDetail workEmailDetail; + + private WorkOrganizationNameDetail organizationDetail; + + private JobTitleDetail jobTitleDetail; + + private AboutMeDetail aboutMeDetail; - private BinaryDetail avatarDetail; + private GenderDetail genderDetail; - private byte[] newAvatarImage; + private BirthDateDetail birthDateDetail; - private ImageIcon avatarImageIcon = null; + private ImageDetail avatarDetail; /** - * The last avatar file directory open. + * The panel that contains description labels and text fields + * for every account detail. */ - private File lastAvatarDir; + private JPanel valuesPanel; + + private JScrollPane mainScrollPane; /** * Construct a panel containing all account details for the given protocol @@ -110,10 +208,11 @@ public class AccountDetailsPanel */ public AccountDetailsPanel(ProtocolProviderService protocolProvider) { - super(new BorderLayout()); - + setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + setOpaque(false); + this.setPreferredSize(new Dimension(600, 400)); + this.protocolProvider = protocolProvider; this.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); -// this.setPreferredSize(new Dimension(500, 400)); accountInfoOpSet = protocolProvider @@ -134,550 +233,931 @@ public AccountDetailsPanel(ProtocolProviderService protocolProvider) } } + /** + * Creates the panel that indicates to the user that the currently selected + * contact does not support server stored contact info. + */ + private void initUnsupportedPanel() + { + JTextArea unsupportedTextArea = + new JTextArea(Resources.getString( + "plugin.accountinfo.NOT_SUPPORTED")); + + unsupportedTextArea.setEditable(false); + unsupportedTextArea.setOpaque(false); + + JPanel unsupportedPanel + = new TransparentPanel(); + + unsupportedPanel.setBorder( + BorderFactory.createEmptyBorder(50, 20, 50, 20)); + + unsupportedPanel.add(unsupportedTextArea); + + this.add(unsupportedPanel, BorderLayout.NORTH); + } + + /** + * Initialized the main panel that contains all ServerStoredDetails + */ private void initSummaryPanel() { JPanel summaryPanel = new TransparentPanel(new BorderLayout(10, 10)); summaryPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); - summaryPanel.setSize(this.getWidth(), this.getHeight()); // Create the avatar panel. JPanel leftPanel = new TransparentPanel(new BorderLayout()); JPanel avatarPanel = new TransparentPanel(new BorderLayout()); - JButton changeAvatarButton = new JButton(Resources.getString("plugin.accountinfo.CHANGE")); - JPanel changeButtonPanel - = new TransparentPanel(new FlowLayout(FlowLayout.CENTER)); - - changeAvatarButton.addActionListener(new ChangeAvatarActionListener()); - changeButtonPanel.add(changeAvatarButton); - - avatarPanel.add(avatarLabel, BorderLayout.CENTER); - avatarPanel.add(changeButtonPanel, BorderLayout.SOUTH); + JPanel radioButtonPanel = new TransparentPanel(new GridLayout(2, 1)); + globalIcon = + new JRadioButton( + Resources.getString("plugin.accountinfo.GLOBAL_ICON")); + globalIcon.setSelected(true); + globalIcon.setOpaque(false); + globalIcon.setEnabled(false); + localIcon = + new JRadioButton( + Resources.getString("plugin.accountinfo.LOCAL_ICON")); + localIcon.setOpaque(false); + localIcon.setEnabled(false); + ButtonGroup group = new ButtonGroup(); + group.add(globalIcon); + group.add(localIcon); + radioButtonPanel.add(globalIcon); + radioButtonPanel.add(localIcon); + avatarPanel.add(radioButtonPanel, BorderLayout.NORTH); leftPanel.add(avatarPanel, BorderLayout.NORTH); - summaryPanel.add(leftPanel, BorderLayout.WEST); + detailToTextField.put(ImageDetail.class, new JTextField()); + + imageWithMenu + = new FramedImageWithMenu( + Resources.getImage( + "service.gui.DEFAULT_USER_PHOTO"), + Resources.getImage( + "service.gui.DEFAULT_USER_PHOTO").getIconWidth(), + Resources.getImage( + "service.gui.DEFAULT_USER_PHOTO").getIconHeight()); + SelectAvatarMenu selectAvatarMenu = new SelectAvatarMenu(imageWithMenu); + selectAvatarMenu.setAccountID(protocolProvider.getAccountID()); + imageWithMenu.setPopupMenu(selectAvatarMenu); + avatarPanel.add(imageWithMenu, BorderLayout.SOUTH); + + globalIcon.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + imageWithMenu.setEnabled(false); + } + }); + localIcon.addActionListener(new ActionListener() + { + @Override + public void actionPerformed(ActionEvent e) + { + imageWithMenu.setEnabled(true); + } + }); + imageWithMenu.setEnabled(false); + + valuesPanel = new TransparentPanel(new GridBagLayout()); + valuesPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + + GridBagConstraints first = new GridBagConstraints(); + first.gridx = 0; + first.gridy = 0; + first.weightx = 0; + first.anchor = GridBagConstraints.LINE_START; + first.gridwidth = 1; + first.insets = new Insets(4, 4, 4, 4); + first.fill = GridBagConstraints.HORIZONTAL; + GridBagConstraints second = new GridBagConstraints(); + second.gridx = 1; + second.gridy = 0; + second.weightx = 2; + second.anchor = GridBagConstraints.LINE_START; + second.gridwidth = 1; // GridBagConstraints.REMAINDER; + second.insets = first.insets; + second.fill = GridBagConstraints.HORIZONTAL; + + if (accountInfoOpSet.isDetailClassSupported(DisplayNameDetail.class)) + { + displayNameField = new JTextField(); + valuesPanel.add(new JLabel( + Resources.getString("plugin.accountinfo.DISPLAY_NAME")) + , first); + valuesPanel.add(displayNameField, second); + first.gridy = ++second.gridy; + detailToTextField.put(DisplayNameDetail.class, displayNameField); + } - // Create the summary details panel. - JPanel detailsPanel = new TransparentPanel(new BorderLayout(10, 10)); - detailsPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + firstNameField = new JTextField(); + valuesPanel.add(new JLabel( + Resources.getString("plugin.accountinfo.FIRST_NAME")) + , first); + valuesPanel.add(firstNameField, second); + first.gridy = ++second.gridy; + detailToTextField.put(FirstNameDetail.class, firstNameField); + + middleNameField = new JTextField(); + valuesPanel.add(new JLabel( + Resources.getString("plugin.accountinfo.MIDDLE_NAME")) + , first); + valuesPanel.add(middleNameField, second); + first.gridy = ++second.gridy; + detailToTextField.put(MiddleNameDetail.class, middleNameField); + + lastNameField = new JTextField(); + valuesPanel.add(new JLabel( + Resources.getString("plugin.accountinfo.LAST_NAME")) + , first); + valuesPanel.add(lastNameField, second); + first.gridy = ++second.gridy; + detailToTextField.put(LastNameDetail.class, lastNameField); + + if (accountInfoOpSet.isDetailClassSupported(NicknameDetail.class)) + { + nicknameField = new JTextField(); + valuesPanel.add(new JLabel( + Resources.getString("plugin.accountinfo.NICKNAME")) + , first); + valuesPanel.add(nicknameField, second); + first.gridy = ++second.gridy; + detailToTextField.put(NicknameDetail.class, nicknameField); + } + if (accountInfoOpSet.isDetailClassSupported(URLDetail.class)) + { + urlField = new JTextField(); + valuesPanel.add(new JLabel( + Resources.getString("plugin.accountinfo.URL")) + , first); + valuesPanel.add(urlField, second); + first.gridy = ++second.gridy; + detailToTextField.put(URLDetail.class, urlField); + } + if (accountInfoOpSet.isDetailClassSupported(GenderDetail.class)) + { + genderField = new JTextField(); + valuesPanel.add(new JLabel( + Resources.getString("plugin.accountinfo.GENDER")) + , first); + valuesPanel.add(genderField, second); + first.gridy = ++second.gridy; + detailToTextField.put(GenderDetail.class, genderField); + } - summaryPanel.add(detailsPanel, BorderLayout.CENTER); + birthDayCalendar = new JDateChooser(); + valuesPanel.add(new JLabel( + Resources.getString("plugin.accountinfo.BDAY")) + , first); + valuesPanel.add(birthDayCalendar, second); + birthDayCalendar.setDateFormatString( + Resources.getString("plugin.accountinfo.BDAY_FORMAT")); + birthDayCalendar.addPropertyChangeListener( + new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) + { + if (evt.getPropertyName().equals("date")) + { + Date date = (Date) evt.getNewValue(); + if (date != null) + { + Calendar currentDate = Calendar.getInstance(); + Calendar c = Calendar.getInstance(); + c.setTime(date); + int age = + currentDate.get(Calendar.YEAR) - + c.get(Calendar.YEAR); + + if (currentDate.get(Calendar.MONTH) < + c.get(Calendar.MONTH)) + age--; + if ((currentDate.get(Calendar.MONTH) == + c.get(Calendar.MONTH)) + && + (currentDate.get(Calendar.DAY_OF_MONTH) < + c.get(Calendar.DAY_OF_MONTH))) + age--; + String ageDetail = Integer.toString(age).trim(); + ageField.setText(ageDetail); + } + } + } + }); + first.gridy = ++second.gridy; + ageField = new JTextField(); + valuesPanel.add(new JLabel( + Resources.getString("plugin.accountinfo.AGE")), first); + valuesPanel.add(ageField, second); + first.gridy = ++second.gridy; + ageField.setEditable(false); + detailToTextField.put(BirthDateDetail.class, new JTextField()); + + if (accountInfoOpSet.isDetailClassSupported(AddressDetail.class)) + { + streetAddressField = new JTextField(); + valuesPanel.add(new JLabel( + Resources.getString("plugin.accountinfo.STREET")) + , first); + valuesPanel.add(streetAddressField, second); + first.gridy = ++second.gridy; + detailToTextField.put(AddressDetail.class, streetAddressField); + } + if (accountInfoOpSet.isDetailClassSupported(CityDetail.class)) + { + cityField = new JTextField(); + valuesPanel.add(new JLabel( + Resources.getString("plugin.accountinfo.CITY")), first); + valuesPanel.add(cityField, second); + first.gridy = ++second.gridy; + detailToTextField.put(CityDetail.class, cityField); + } + if (accountInfoOpSet.isDetailClassSupported(ProvinceDetail.class)) + { + regionField = new JTextField(); + valuesPanel.add(new JLabel( + Resources.getString("plugin.accountinfo.REGION")) + , first); + valuesPanel.add(regionField, second); + first.gridy = ++second.gridy; + detailToTextField.put(ProvinceDetail.class, regionField); + } + if (accountInfoOpSet.isDetailClassSupported(PostalCodeDetail.class)) + { + postalCodeField = new JTextField(); + valuesPanel.add(new JLabel( + Resources.getString("plugin.accountinfo.POST")) + , first); + valuesPanel.add(postalCodeField, second); + first.gridy = ++second.gridy; + detailToTextField.put(PostalCodeDetail.class, postalCodeField); + } + if (accountInfoOpSet.isDetailClassSupported(CountryDetail.class)) + { + countryField = new JTextField(); + valuesPanel.add(new JLabel( + Resources.getString("plugin.accountinfo.COUNTRY")) + , first); + valuesPanel.add(countryField, second); + first.gridy = ++second.gridy; + detailToTextField.put(CountryDetail.class, countryField); + } - // Labels panel. - JPanel labelsPanel = new TransparentPanel(new GridLayout(0, 1, 5, 5)); + emailField = new JTextField(); + valuesPanel.add(new JLabel( + Resources.getString("plugin.accountinfo.EMAIL")) + , first); + valuesPanel.add(emailField, second); + first.gridy = ++second.gridy; + detailToTextField.put(EmailAddressDetail.class, emailField); - labelsPanel.add(new JLabel( - Resources.getString("plugin.accountinfo.FIRST_NAME"))); -// labelsPanel.add(new JLabel(Resources.getString("plugin.accountinfo.MIDDLE_NAME"))); - labelsPanel.add(new JLabel( - Resources.getString("plugin.accountinfo.LAST_NAME"))); - labelsPanel.add(new JLabel( - Resources.getString("plugin.accountinfo.GENDER"))); - labelsPanel.add(new JLabel( - Resources.getString("plugin.accountinfo.AGE"))); - labelsPanel.add(new JLabel( - Resources.getString("plugin.accountinfo.BDAY"))); - labelsPanel.add(new JLabel( - Resources.getString("plugin.accountinfo.EMAIL"))); - labelsPanel.add(new JLabel( - Resources.getString("plugin.accountinfo.PHONE"))); + if (accountInfoOpSet.isDetailClassSupported( + WorkEmailAddressDetail.class)) + { + workEmailField = new JTextField(); + valuesPanel.add(new JLabel( + Resources.getString("plugin.accountinfo.WORK_EMAIL")) + , first); + valuesPanel.add(workEmailField, second); + first.gridy = ++second.gridy; + detailToTextField.put(WorkEmailAddressDetail.class, workEmailField); + } - detailsPanel.add(labelsPanel, BorderLayout.WEST); + phoneField = new JTextField(); + valuesPanel.add(new JLabel( + Resources.getString("plugin.accountinfo.PHONE")) + , first); + valuesPanel.add(phoneField, second); + first.gridy = ++second.gridy; + detailToTextField.put(PhoneNumberDetail.class, phoneField); - // Values panel. - JPanel valuesPanel = new TransparentPanel(new GridLayout(0, 1, 5, 5)); + if (accountInfoOpSet.isDetailClassSupported(WorkPhoneDetail.class)) + { + workPhoneField = new JTextField(); + valuesPanel.add(new JLabel( + Resources.getString("plugin.accountinfo.WORK_PHONE")) + , first); + valuesPanel.add(workPhoneField, second); + first.gridy = ++second.gridy; + detailToTextField.put( + WorkPhoneDetail.class, workPhoneField); + } + if (accountInfoOpSet.isDetailClassSupported( + WorkOrganizationNameDetail.class)) + { + organizationField = new JTextField(); + valuesPanel.add(new JLabel( + Resources.getString("plugin.accountinfo.ORGANIZATION")) + , first); + valuesPanel.add(organizationField, second); + first.gridy = ++second.gridy; + detailToTextField.put( + WorkOrganizationNameDetail.class, organizationField); + } + if (accountInfoOpSet.isDetailClassSupported(JobTitleDetail.class)) + { + jobTitleField = new JTextField(); + valuesPanel.add(new JLabel( + Resources.getString("plugin.accountinfo.JOB_TITLE")) + , first); + valuesPanel.add(jobTitleField, second); + first.gridy = ++second.gridy; + detailToTextField.put( + JobTitleDetail.class, jobTitleField); + } + if (accountInfoOpSet.isDetailClassSupported(AboutMeDetail.class)) + { + aboutMeArea = new JTextArea(3, 0); + valuesPanel.add(new JLabel( + Resources.getString("plugin.accountinfo.ABOUT_ME")) + , first); + second.gridheight = 3; + JScrollPane areaScrollPane = new JScrollPane(aboutMeArea); + areaScrollPane.setOpaque(false); + areaScrollPane.setBorder(BorderFactory.createEmptyBorder()); + areaScrollPane.setVerticalScrollBarPolicy( + JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); + valuesPanel.add(areaScrollPane, second); + first.gridy = ++second.gridy; + + DefaultStyledDocument doc = new DefaultStyledDocument(); + doc.setDocumentFilter(new DocumentFilter() { + + private final int MAX_CHARACTERS = + Integer.valueOf(Resources.getString( + "plugin.accountinfo.ABOUT_ME_MAX_CHARACTERS")); + + public void insertString(FilterBypass fb, int offs, + String str, AttributeSet a) + throws BadLocationException { + //This rejects the entire insertion if it would make + //the contents too long. Another option would be + //to truncate the inserted string so the contents + //would be exactly maxCharacters in length. + if ((fb.getDocument().getLength() + str.length()) + <= MAX_CHARACTERS) + super.insertString(fb, offs, str, a); + else + Toolkit.getDefaultToolkit().beep(); + } - valuesPanel.add(firstNameField); -// valuesPanel.add(middleNameField); - valuesPanel.add(lastNameField); - valuesPanel.add(genderField); - valuesPanel.add(ageField); - valuesPanel.add(birthdayField); - valuesPanel.add(emailField); - valuesPanel.add(phoneField); + public void replace(FilterBypass fb, int offs, + int length, + String str, AttributeSet a) + throws BadLocationException { + //This rejects the entire replacement if it would make + //the contents too long. Another option would be + //to truncate the replacement string so the contents + //would be exactly maxCharacters in length. + if ((fb.getDocument().getLength() + str.length() + - length) <= MAX_CHARACTERS) + super.replace(fb, offs, length, str, a); + else + Toolkit.getDefaultToolkit().beep(); + } + }); + + aboutMeArea.setDocument(doc); + aboutMeArea.setBorder(firstNameField.getBorder()); + aboutMeArea.setLineWrap(true); + aboutMeArea.setWrapStyleWord(true); + aboutMeArea.setPreferredSize(new Dimension(50, 100)); + aboutMeArea.setMinimumSize(new Dimension(50, 100)); + detailToTextField.put(AboutMeDetail.class, new JTextField()); + } - detailsPanel.add(valuesPanel, BorderLayout.CENTER); + mainScrollPane = new JScrollPane(summaryPanel, + JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, + JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + mainScrollPane.getViewport().setOpaque(false); + mainScrollPane.setOpaque(false); + mainScrollPane.setBorder(BorderFactory.createEmptyBorder()); - this.mainScrollPane.getViewport().add(summaryPanel); + summaryPanel.add(valuesPanel, BorderLayout.CENTER); - this.add(mainScrollPane, BorderLayout.NORTH); + this.add(mainScrollPane); this.applyButton.addActionListener(new SubmitActionListener()); + JButton cancelButton = + new JButton(Resources.getString("service.gui.CANCEL")); + cancelButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent evt) + { + AccountInfoMenuItemComponent.dialog.setVisible(false); + mainScrollPane.getVerticalScrollBar().setValue(0); + } + }); this.buttonPanel.add(applyButton); + this.buttonPanel.add(cancelButton); - this.add(buttonPanel, BorderLayout.SOUTH); + this.add(buttonPanel); - // All items are now instantiated and could safely load the skin. - loadSkin(); + for (Component component : valuesPanel.getComponents()) + component.setEnabled(false); + if (aboutMeArea != null) + aboutMeArea.setEnabled(false); + applyButton.setEnabled(false); } /** - * Loads details for + * Loads all ServerStoredDetails which are currently supported by + * this plugin. Note that some OperationSetServerStoredAccountInfo + * implementations may support details that are not supported by this plugin. + * In this case they will not be loaded. */ public void loadDetails() { - this.loadSummaryDetails(); - this.isDataLoaded = true; + if (accountInfoOpSet != null) + { + Iterator allDetails = + accountInfoOpSet.getAllAvailableDetails(); + + while (allDetails.hasNext()) + { + GenericDetail detail = allDetails.next(); + loadDetail(detail); + } + + boolean isAnyDetailEditable = false; + for (Class editable : + detailToTextField.keySet()) + { + if (accountInfoOpSet.isDetailClassEditable(editable)) + { + isAnyDetailEditable = true; + if (editable.equals(AboutMeDetail.class)) + aboutMeArea.setEnabled(true); + else if (editable.equals(BirthDateDetail.class)) + birthDayCalendar.setEnabled(true); + else if (editable.equals(ImageDetail.class)) + { + globalIcon.setEnabled(true); + localIcon.setEnabled(true); + imageWithMenu.setEnabled(true); + } + else + { + JTextField field = detailToTextField.get(editable); + field.setEnabled(true); + } + } + } + if (isAnyDetailEditable) + applyButton.setEnabled(true); + } } /** - * Creates the panel that indicates to the user that the currently selected - * contact does not support server stored contact info. + * Loads a single GenericDetail obtained from the + * OperationSetServerStoredAccountInfo into this plugin. + * @param detail to be loaded. */ - private void initUnsupportedPanel() + private void loadDetail(GenericDetail detail) { - JTextArea unsupportedTextArea = - new JTextArea(Resources.getString( - "plugin.accountinfo.NOT_SUPPORTED")); - - unsupportedTextArea.setEditable(false); - unsupportedTextArea.setLineWrap(true); - - JPanel unsupportedPanel - = new TransparentPanel(new FlowLayout(FlowLayout.CENTER)); - - unsupportedTextArea.setPreferredSize(new Dimension(200, 200)); + if (detail.getClass().equals(AboutMeDetail.class)) + { + aboutMeDetail = (AboutMeDetail) detail; + aboutMeArea.setText((String) detail.getDetailValue()); + return; + } + if (detail instanceof BirthDateDetail) + { + birthDateDetail = (BirthDateDetail) detail; - unsupportedPanel.setBorder( - BorderFactory.createEmptyBorder(50, 20, 50, 20)); + Calendar calendarDetail = + (Calendar) birthDateDetail.getDetailValue(); - unsupportedPanel.add(unsupportedTextArea); + birthDayCalendar.setCalendar(calendarDetail); + return; + } - this.add(unsupportedPanel); + JTextField field = detailToTextField.get(detail.getClass()); + if (field != null) + { + if (detail instanceof ImageDetail) + { + localIcon.setSelected(true); + avatarDetail = (ImageDetail) detail; + byte[] avatarImage = avatarDetail.getBytes(); + imageWithMenu.setImageIcon(avatarImage); + } + else if (detail instanceof URLDetail) + { + urlDetail = (URLDetail) detail; + urlField.setText(urlDetail.getURL().toString()); + } + else + { + field.setText((String) detail.getDetailValue()); + if (detail.getClass().equals(DisplayNameDetail.class)) + displayNameDetail = (DisplayNameDetail) detail; + else if (detail.getClass().equals(FirstNameDetail.class)) + firstNameDetail = (FirstNameDetail) detail; + else if (detail.getClass().equals(MiddleNameDetail.class)) + middleNameDetail = (MiddleNameDetail) detail; + else if (detail.getClass().equals(LastNameDetail.class)) + lastNameDetail = (LastNameDetail) detail; + else if (detail.getClass().equals(NicknameDetail.class)) + nicknameDetail = (NicknameDetail) detail; + else if (detail.getClass().equals(URLDetail.class)) + urlDetail = (URLDetail) detail; + else if (detail.getClass().equals(GenderDetail.class)) + genderDetail = (GenderDetail) detail; + else if (detail.getClass().equals(AddressDetail.class)) + streetAddressDetail = (AddressDetail) detail; + else if (detail.getClass().equals(CityDetail.class)) + cityDetail = (CityDetail) detail; + else if (detail.getClass().equals(ProvinceDetail.class)) + regionDetail = (ProvinceDetail) detail; + else if (detail.getClass().equals(PostalCodeDetail.class)) + postalCodeDetail = (PostalCodeDetail) detail; + else if (detail.getClass().equals(CountryDetail.class)) + countryDetail = (CountryDetail) detail; + else if (detail.getClass().equals(PhoneNumberDetail.class)) + phoneDetail = (PhoneNumberDetail) detail; + else if (detail.getClass().equals(WorkPhoneDetail.class)) + workPhoneDetail = (WorkPhoneDetail) detail; + else if (detail.getClass().equals(EmailAddressDetail.class)) + emailDetail = (EmailAddressDetail) detail; + else if (detail.getClass().equals(WorkEmailAddressDetail.class)) + workEmailDetail = (WorkEmailAddressDetail) detail; + else if (detail.getClass().equals( + WorkOrganizationNameDetail.class)) + organizationDetail = (WorkOrganizationNameDetail) detail; + else if (detail.getClass().equals(JobTitleDetail.class)) + jobTitleDetail = (JobTitleDetail) detail; + else if (detail.getClass().equals(AboutMeDetail.class)) + aboutMeDetail = (AboutMeDetail) detail; + } + } } /** - * Creates a panel that can be added as the summary tab that displays the - * following details: - - *

- * Avatar(Contact image) - FirstNameDetail - MiddleNameDetail - - * LastNameDetail - BirthdateDetail (and calculate age) - GenderDetail - - * EmailAddressDetail - PhoneNumberDetail. All other details will be* added - * to our list of extended details. + * Attempts to upload all ServerStoredDetails on the server using + * OperationSetServerStoredAccountInfo + * */ - private void loadSummaryDetails() + private class SubmitActionListener implements ActionListener { - Iterator contactDetails; - - // Avatar details. - contactDetails - = accountInfoOpSet.getDetails(BinaryDetail.class); - - byte[] avatarImage = null; - if (contactDetails.hasNext()) + public void actionPerformed(ActionEvent e) { - avatarDetail = (BinaryDetail) contactDetails.next(); + if (accountInfoOpSet.isDetailClassSupported(ImageDetail.class)) + { + if (globalIcon.isSelected()) + { + GlobalDisplayDetailsService serv = + AccountInfoActivator.getGlobalDisplayDetailsService(); - avatarImage = avatarDetail.getBytes(); - } + byte[] newIcon = serv.getGlobalDisplayAvatar(); - if (avatarImage != null && avatarImage.length > 0) - { - avatarImageIcon = new ImageIcon( - getScaledImageInstance(avatarImage)); - avatarLabel.setIcon(avatarImageIcon); - } + ImageDetail newDetail = null; + if (newIcon != null) + newDetail = + new ImageDetail( + "avatar", serv.getGlobalDisplayAvatar()); - // First name details. - contactDetails = - accountInfoOpSet.getDetails(FirstNameDetail.class); + if (avatarDetail != null || newDetail != null) + changeDetail(avatarDetail, newDetail); + } + } + if (accountInfoOpSet.isDetailClassSupported + (DisplayNameDetail.class)) + { + String text = + detailToTextField.get(DisplayNameDetail.class).getText(); - String firstNameDetailString = ""; - while (contactDetails.hasNext()) - { - firstNameDetail = (FirstNameDetail) contactDetails.next(); + DisplayNameDetail newDetail = null; + if (!StringUtils.isNullOrEmpty(text, true)) + newDetail = new DisplayNameDetail(text); - firstNameDetailString = - firstNameDetailString + " " + firstNameDetail.getDetailValue(); - } + if (displayNameDetail != null || newDetail != null) + changeDetail(displayNameDetail, newDetail); + } + if (accountInfoOpSet.isDetailClassSupported( + FirstNameDetail.class)) + { + String text = + detailToTextField.get(FirstNameDetail.class).getText(); - firstNameField.setText(firstNameDetailString); + FirstNameDetail newDetail = null; + if (!StringUtils.isNullOrEmpty(text, true)) + newDetail = new FirstNameDetail(text); - // Middle name details. - contactDetails = - accountInfoOpSet.getDetails(MiddleNameDetail.class); + if (firstNameDetail != null || newDetail != null) + changeDetail(firstNameDetail, newDetail); + } + if (accountInfoOpSet.isDetailClassSupported( + MiddleNameDetail.class)) + { + String text = + detailToTextField.get(MiddleNameDetail.class).getText(); - String middleNameDetailString = ""; - while (contactDetails.hasNext()) - { - middleNameDetail = (MiddleNameDetail) contactDetails.next(); - middleNameDetailString = - middleNameDetailString + " " + middleNameDetail.getDetailValue(); - } + MiddleNameDetail newDetail = null; + if (!StringUtils.isNullOrEmpty(text, true)) + newDetail = new MiddleNameDetail(text); - middleNameField.setText(middleNameDetailString); + if (middleNameDetail != null || newDetail != null) + changeDetail(middleNameDetail, newDetail); + } + if (accountInfoOpSet.isDetailClassSupported( + LastNameDetail.class)) + { + String text = + detailToTextField.get(LastNameDetail.class).getText(); + LastNameDetail newDetail = null; - // Last name details. - contactDetails = - accountInfoOpSet.getDetails(LastNameDetail.class); + if (!StringUtils.isNullOrEmpty(text, true)) + newDetail = new LastNameDetail(text); - String lastNameDetailString = ""; - while (contactDetails.hasNext()) - { - lastNameDetail = (LastNameDetail) contactDetails.next(); + if (lastNameDetail != null || newDetail != null) + changeDetail(lastNameDetail, newDetail); + } + if (accountInfoOpSet.isDetailClassSupported(NicknameDetail.class)) + { + String text = + detailToTextField.get(NicknameDetail.class).getText(); - lastNameDetailString = - lastNameDetailString + " " + lastNameDetail.getDetailValue(); - } + NicknameDetail newDetail = null; + if (!StringUtils.isNullOrEmpty(text, true)) + newDetail = new NicknameDetail(text); - lastNameField.setText(lastNameDetailString); + if (nicknameDetail != null || newDetail != null) + changeDetail(nicknameDetail, newDetail); + } - // Gender details. - contactDetails = - accountInfoOpSet.getDetails(GenderDetail.class); + if (accountInfoOpSet.isDetailClassSupported(URLDetail.class)) + { + String text + = detailToTextField.get(URLDetail.class).getText(); - String genderDetailString = ""; - while (contactDetails.hasNext()) - { - genderDetail = (GenderDetail) contactDetails.next(); - genderDetailString = genderDetailString + " " - + genderDetail.getDetailValue(); - } + URL url = null; + if (!StringUtils.isNullOrEmpty(text, true)) + try + { + url = new URL(text); + } + catch (MalformedURLException e1) + { + logger.debug("Failed to update URL detail due to " + + "malformed URL."); + } - genderField.setText(genderDetailString); + URLDetail newDetail = null; - // Birthday details. - contactDetails = - accountInfoOpSet.getDetails(BirthDateDetail.class); + if (url != null) + newDetail = new URLDetail("URL", url); - String birthDateDetailString = ""; - String ageDetail = ""; - if (contactDetails.hasNext()) - { - birthDateDetail = (BirthDateDetail) contactDetails.next(); + if (urlDetail != null || newDetail != null) + changeDetail(urlDetail, newDetail); + } - Calendar calendarDetail = - (Calendar) birthDateDetail.getDetailValue(); + if (accountInfoOpSet.isDetailClassSupported( + GenderDetail.class)) + { + String text = + detailToTextField.get(GenderDetail.class).getText(); - Date birthDate = calendarDetail.getTime(); - DateFormat dateFormat = DateFormat.getDateInstance(); + GenderDetail newDetail = null; - birthDateDetailString = dateFormat.format(birthDate).trim(); + if (!StringUtils.isNullOrEmpty(text, true)) + newDetail = new GenderDetail(text); - Calendar c = Calendar.getInstance(); - int age = c.get(Calendar.YEAR) - calendarDetail.get(Calendar.YEAR); + if (genderDetail != null || newDetail != null) + changeDetail(genderDetail, newDetail); + } - if (c.get(Calendar.MONTH) < calendarDetail.get(Calendar.MONTH)) - age--; + if (accountInfoOpSet.isDetailClassSupported( + BirthDateDetail.class)) + { + BirthDateDetail newDetail = null; - ageDetail = Integer.toString(age).trim(); - } + if (birthDayCalendar.getDate() != null) + { + newDetail = + new BirthDateDetail(birthDayCalendar.getCalendar()); + } - birthdayField.setText(birthDateDetailString); - ageField.setText(ageDetail); + if (birthDateDetail != null || newDetail != null) + changeDetail(birthDateDetail, newDetail); + } - // Email details. - contactDetails = - accountInfoOpSet.getDetails(EmailAddressDetail.class); + if (accountInfoOpSet.isDetailClassSupported( + AddressDetail.class)) + { + String text = + detailToTextField.get(AddressDetail.class).getText(); - String emailDetailString = ""; - while (contactDetails.hasNext()) - { - emailDetail = (EmailAddressDetail) contactDetails.next(); - emailDetailString = emailDetailString + " " - + emailDetail.getDetailValue(); - } + AddressDetail newDetail = null; + if (!StringUtils.isNullOrEmpty(text, true)) + newDetail = new AddressDetail(text); - emailField.setText(emailDetailString); + if (streetAddressDetail != null || newDetail != null) + changeDetail(streetAddressDetail, newDetail); + } - // Phone number details. - contactDetails = - accountInfoOpSet.getDetails(PhoneNumberDetail.class); + if (accountInfoOpSet.isDetailClassSupported( + CityDetail.class)) + { + String text = + detailToTextField.get(CityDetail.class).getText(); - String phoneNumberDetailString = ""; - while (contactDetails.hasNext()) - { - phoneDetail = (PhoneNumberDetail) contactDetails.next(); - phoneNumberDetailString = - phoneNumberDetailString + " " + phoneDetail.getDetailValue(); - } + CityDetail newDetail = null; + if (!StringUtils.isNullOrEmpty(text, true)) + newDetail = new CityDetail(text); - phoneField.setText(phoneNumberDetailString); - } + if (cityDetail != null || newDetail != null) + changeDetail(cityDetail, newDetail); + } - private class SubmitActionListener implements ActionListener - { - public void actionPerformed(ActionEvent e) - { - String firstName = firstNameField.getText(); -// String middleName = middleNameField.getText(); - String lastName = lastNameField.getText(); - String gender = genderField.getText(); - String email = emailField.getText(); - String phoneNumber = phoneField.getText(); + if (accountInfoOpSet.isDetailClassSupported( + ProvinceDetail.class)) + { + String text = + detailToTextField.get(ProvinceDetail.class).getText(); - Calendar birthDateCalendar = Calendar.getInstance(); + ProvinceDetail newDetail = null; + if (!StringUtils.isNullOrEmpty(text, true)) + newDetail = new ProvinceDetail(text); - if(birthdayField.getText() != null - && birthdayField.getText().length() > 0) + if (regionDetail != null || newDetail != null) + changeDetail(regionDetail, newDetail); + } + + if (accountInfoOpSet.isDetailClassSupported( + PostalCodeDetail.class)) { - try - { - DateFormat dateFormat - = DateFormat.getDateInstance(DateFormat.DEFAULT); + String text = + detailToTextField.get(PostalCodeDetail.class).getText(); - Date birthDate = dateFormat.parse(birthdayField.getText()); + PostalCodeDetail newDetail = null; + if (!StringUtils.isNullOrEmpty(text, true)) + newDetail = new PostalCodeDetail(text); - birthDateCalendar.setTime(birthDate); - } - catch (ParseException e2) - { - logger.error("Failed to parse birth date.", e2); - } + if (postalCodeDetail != null || newDetail != null) + changeDetail(postalCodeDetail, newDetail); } - try + if (accountInfoOpSet.isDetailClassSupported(CountryDetail.class)) { - FirstNameDetail newFirstNameDetail - = new ServerStoredDetails.FirstNameDetail(firstName); + String text = detailToTextField.get(CountryDetail.class).getText(); - if (firstNameDetail == null) - accountInfoOpSet.addDetail(newFirstNameDetail); - else - accountInfoOpSet.replaceDetail( firstNameDetail, - newFirstNameDetail); - -// MiddleNameDetail newMiddleNameDetail -// = new ServerStoredDetails.MiddleNameDetail(middleName); -// -// if (middleNameDetail == null) -// accountInfoOpSet.addDetail(newMiddleNameDetail); -// else -// accountInfoOpSet.replaceDetail( middleNameDetail, -// newMiddleNameDetail); - - LastNameDetail newLastNameDetail - = new ServerStoredDetails.LastNameDetail(lastName); - - if (lastNameDetail == null) - accountInfoOpSet.addDetail(newLastNameDetail); - else - accountInfoOpSet.replaceDetail( lastNameDetail, - newLastNameDetail); + CountryDetail newDetail = null; + if (!StringUtils.isNullOrEmpty(text, true)) + newDetail = new CountryDetail(text); - GenderDetail newGenderDetail - = new ServerStoredDetails.GenderDetail(gender); + if (countryDetail != null || newDetail != null) + changeDetail(countryDetail, newDetail); + } - if (genderDetail == null) - accountInfoOpSet.addDetail(newGenderDetail); - else - accountInfoOpSet.replaceDetail( genderDetail, - newGenderDetail); + if (accountInfoOpSet.isDetailClassSupported( + EmailAddressDetail.class)) + { + String text = + detailToTextField.get(EmailAddressDetail.class).getText(); - BirthDateDetail newBirthDateDetail - = new ServerStoredDetails.BirthDateDetail(birthDateCalendar); + EmailAddressDetail newDetail = null; + if (!StringUtils.isNullOrEmpty(text, true)) + newDetail = new EmailAddressDetail(text); - if (birthDateDetail == null) - accountInfoOpSet.addDetail(newBirthDateDetail); - else - accountInfoOpSet.replaceDetail( birthDateDetail, - newBirthDateDetail); + if (emailDetail != null || newDetail != null) + changeDetail(emailDetail, newDetail); + } - EmailAddressDetail newEmailDetail - = new ServerStoredDetails.EmailAddressDetail(email); + if (accountInfoOpSet.isDetailClassSupported( + WorkEmailAddressDetail.class)) + { + String text = + detailToTextField.get(WorkEmailAddressDetail.class) + .getText(); - if (emailDetail == null) - accountInfoOpSet.addDetail(newEmailDetail); - else - accountInfoOpSet.replaceDetail( emailDetail, - newEmailDetail); + WorkEmailAddressDetail newDetail = null; + if (!StringUtils.isNullOrEmpty(text, true)) + newDetail = new WorkEmailAddressDetail(text); - PhoneNumberDetail newPhoneDetail - = new ServerStoredDetails.PhoneNumberDetail(phoneNumber); + if (workEmailDetail != null || newDetail != null) + changeDetail(workEmailDetail, newDetail); + } - if (phoneDetail == null) - accountInfoOpSet.addDetail(newPhoneDetail); - else - accountInfoOpSet.replaceDetail( phoneDetail, - newPhoneDetail); + if (accountInfoOpSet.isDetailClassSupported( + PhoneNumberDetail.class)) + { + String text = + detailToTextField.get(PhoneNumberDetail.class).getText(); - BinaryDetail newAvatarDetail - = new ServerStoredDetails.BinaryDetail( - "Avatar", - newAvatarImage); + PhoneNumberDetail newDetail = null; + if (!StringUtils.isNullOrEmpty(text, true)) + newDetail = new PhoneNumberDetail(text); - if (avatarDetail == null) - accountInfoOpSet.addDetail(newAvatarDetail); - else - accountInfoOpSet.replaceDetail( avatarDetail, - newAvatarDetail); + if (phoneDetail != null || newDetail != null) + changeDetail(phoneDetail, newDetail); } - catch (ClassCastException e1) + if (accountInfoOpSet.isDetailClassSupported( + WorkPhoneDetail.class)) { - logger.error("Failed to update account details.", e1); + String text = + detailToTextField.get(WorkPhoneDetail.class).getText(); + + WorkPhoneDetail newDetail = null; + if (!StringUtils.isNullOrEmpty(text, true)) + newDetail = new WorkPhoneDetail(text); + + if (workPhoneDetail != null || newDetail != null) + changeDetail(workPhoneDetail, newDetail); } - catch (OperationFailedException e1) + if (accountInfoOpSet.isDetailClassSupported( + WorkOrganizationNameDetail.class)) { - logger.error("Failed to update account details.", e1); - } - } - } + String text = + detailToTextField.get(WorkOrganizationNameDetail.class) + .getText(); - private class ChangeAvatarActionListener implements ActionListener - { - public void actionPerformed(ActionEvent e) - { - SipCommFileChooser chooser = GenericFileDialog.create( - null, "Change avatar...", - SipCommFileChooser.LOAD_FILE_OPERATION, - lastAvatarDir.getAbsolutePath()); - chooser.addFilter(new ImageFilter()); + WorkOrganizationNameDetail newDetail = null; + if (!StringUtils.isNullOrEmpty(text, true)) + newDetail = new WorkOrganizationNameDetail(text); - File file = chooser.getFileFromDialog(); + if (organizationDetail != null || newDetail != null) + changeDetail(organizationDetail, newDetail); + } - if (file != null) + if (accountInfoOpSet.isDetailClassSupported(JobTitleDetail.class)) { - try - { - lastAvatarDir = file.getParentFile(); + String text = + detailToTextField.get(JobTitleDetail.class) + .getText(); - FileInputStream in = new FileInputStream(file); - byte[] buffer; + JobTitleDetail newDetail = null; + if (!StringUtils.isNullOrEmpty(text, true)) + newDetail = new JobTitleDetail(text); - try - { - buffer = new byte[in.available()]; - in.read(buffer); - } - finally - { - in.close(); - } - if (buffer == null || buffer.length <= 0) - return; + if (jobTitleDetail != null || newDetail != null) + changeDetail(jobTitleDetail, newDetail); + } + if (accountInfoOpSet.isDetailClassSupported(AboutMeDetail.class)) + { + String text = + aboutMeArea.getText(); - newAvatarImage = buffer; + AboutMeDetail newDetail = null; + if (!StringUtils.isNullOrEmpty(text, true)) + newDetail = new AboutMeDetail(text); - avatarImageIcon = new ImageIcon( - getScaledImageInstance(newAvatarImage)); - avatarLabel.setIcon(avatarImageIcon); - } - catch (IOException ex) - { - logger.error("Failed to load image.", ex); - } + if (aboutMeDetail != null || newDetail != null) + changeDetail(aboutMeDetail, newDetail); + } + + try + { + AccountInfoMenuItemComponent.dialog.setVisible(false); + mainScrollPane.getVerticalScrollBar().setValue(0); + accountInfoOpSet.save(); + } + catch (OperationFailedException e1) + { + logger.debug("Failed to update account details.", e1); } } - } - /** - * A custom filter that would accept only image files. - */ - private static class ImageFilter extends SipCommFileFilter - { /** - * Accept all directories and all gif, jpg, tiff, or png files. - * Method implemented from FileFilter abstract class. - * - * @param f a file to accept or not + * A helper method to decide whether to add new + * ServerStoredDetails or to replace an old one. + * @param oldDetail the detail to be replaced. + * @param newDetail the replacement. */ - @Override - public boolean accept(File f) + private void changeDetail( GenericDetail oldDetail, + GenericDetail newDetail) { - if (f.isDirectory()) - { - return true; - } - - String extension = getExtension(f); - if (extension != null) + try { - if (extension.equals("tiff") || - extension.equals("tif") || - extension.equals("gif") || - extension.equals("jpeg") || - extension.equals("jpg") || - extension.equals("png")) + if (newDetail == null) + { + accountInfoOpSet.removeDetail(oldDetail); + } + else if (oldDetail == null) { - return true; + accountInfoOpSet.addDetail(newDetail); } else { - return false; + accountInfoOpSet.replaceDetail(oldDetail, newDetail); } } - - return false; - } - - /** - * Get the extension of a file. - * - * @param f the file - * @return the extension of the file - */ - public String getExtension(File f) - { - String ext = null; - String s = f.getName(); - int i = s.lastIndexOf('.'); - - if (i > 0 && i < s.length() - 1) { - ext = s.substring(i+1).toLowerCase(); + catch (ArrayIndexOutOfBoundsException e1) + { + logger.debug("Failed to update account details. " + + newDetail.getDetailDisplayName() + " " + e1); + } + catch (OperationFailedException e1) + { + logger.debug("Failed to update account details. " + + newDetail.getDetailDisplayName() + " " + e1); } - return ext; - } - - /** - * The description of this filter. - */ - @Override - public String getDescription() - { - return Resources.getString("plugin.accountinfo.ONLY_MESSAGE"); - } - - } - - /** - * Returns a scaled Image instance of the given byte image. - * - * @param image the image in bytes - * @return a scaled Image instance of the given byte image. - */ - private Image getScaledImageInstance(byte[] image) - { - Image resultImage = null; - - try - { - resultImage = ImageIO.read( - new ByteArrayInputStream(image)); - } - catch (Exception e) - { - logger.error("Failed to convert bytes to image.", e); } - - if(resultImage == null) - return null; - - return resultImage.getScaledInstance( - avatarLabel.getWidth(), - avatarLabel.getHeight(), - Image.SCALE_SMOOTH); - } - - /** - * Returns true if the account details are loaded, - * false - otherwise. - * - * @return true if the account details are loaded, - * false - otherwise - */ - public boolean isDataLoaded() - { - return isDataLoaded; - } - - /** - * Loads the avatar default icon. - */ - public void loadSkin() - { - avatarLabel.setIcon(Resources.getImage("accountInfoDefaultPersonIcon")); } } diff --git a/src/net/java/sip/communicator/plugin/accountinfo/AccountInfoActivator.java b/src/net/java/sip/communicator/plugin/accountinfo/AccountInfoActivator.java index 9f34c9ca9..6e1692b8e 100644 --- a/src/net/java/sip/communicator/plugin/accountinfo/AccountInfoActivator.java +++ b/src/net/java/sip/communicator/plugin/accountinfo/AccountInfoActivator.java @@ -8,7 +8,8 @@ import java.util.*; -import net.java.sip.communicator.service.browserlauncher.*; +import net.java.sip.communicator.service.globaldisplaydetails.*; +import net.java.sip.communicator.service.gui.*; import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.util.*; @@ -18,6 +19,7 @@ * Starts the account info bundle. * * @author Adam Glodstein + * @author Marin Dzhigarov */ public class AccountInfoActivator implements BundleActivator @@ -30,21 +32,52 @@ public class AccountInfoActivator */ public static BundleContext bundleContext; - private static BrowserLauncherService browserLauncherService; + private static GlobalDisplayDetailsService globalDisplayDetailsService; public void start(BundleContext bc) throws Exception { AccountInfoActivator.bundleContext = bc; -// new LazyConfigurationForm( -// "net.java.sip.communicator.plugin.accountinfo.AccountInfoPanel", -// getClass().getClassLoader(), "plugin.accountinfo.PLUGIN_ICON", -// "plugin.accountinfo.TITLE"); + Hashtable containerFilter + = new Hashtable(); + containerFilter.put( + Container.CONTAINER_ID, + Container.CONTAINER_TOOLS_MENU.getID()); + + bundleContext.registerService( + PluginComponentFactory.class.getName(), + new PluginComponentFactory(Container.CONTAINER_TOOLS_MENU) + { + @Override + protected PluginComponent getPluginInstance() + { + return new AccountInfoMenuItemComponent( + getContainer(), this); + } + }, + containerFilter); + + containerFilter = new Hashtable(); + containerFilter.put( + Container.CONTAINER_ID, + Container.CONTAINER_ACCOUNT_RIGHT_BUTTON_MENU.getID()); + + bundleContext.registerService( + PluginComponentFactory.class.getName(), + new PluginComponentFactory( + Container.CONTAINER_ACCOUNT_RIGHT_BUTTON_MENU) + { + @Override + protected PluginComponent getPluginInstance() + { + return new AccountInfoMenuItemComponent( + getContainer(), this); + } + }, + containerFilter); } - public void stop(BundleContext bc) throws Exception - { - } + public void stop(BundleContext bc) throws Exception {} /** * Returns all ProtocolProviderFactorys obtained from the bundle @@ -53,7 +86,8 @@ public void stop(BundleContext bc) throws Exception * @return all ProtocolProviderFactorys obtained from the bundle * context */ - public static Map getProtocolProviderFactories() + public static Map + getProtocolProviderFactories() { Map providerFactoriesMap = new Hashtable(); @@ -87,23 +121,21 @@ public static Map getProtocolProviderFactories( } /** - * Returns the BrowserLauncherService currently registered. + * Returns the GlobalDisplayDetailsService obtained from the bundle + * context. * - * @return the BrowserLauncherService + * @return the GlobalDisplayDetailsService obtained from the bundle + * context */ - public static BrowserLauncherService getBrowserLauncher() + public static GlobalDisplayDetailsService getGlobalDisplayDetailsService() { - if (browserLauncherService == null) + if (globalDisplayDetailsService == null) { - ServiceReference serviceReference = - bundleContext.getServiceReference(BrowserLauncherService.class - .getName()); - - browserLauncherService = - (BrowserLauncherService) bundleContext - .getService(serviceReference); + globalDisplayDetailsService + = ServiceUtils.getService( + bundleContext, + GlobalDisplayDetailsService.class); } - - return browserLauncherService; + return globalDisplayDetailsService; } } diff --git a/src/net/java/sip/communicator/plugin/accountinfo/AccountInfoMenuItemComponent.java b/src/net/java/sip/communicator/plugin/accountinfo/AccountInfoMenuItemComponent.java new file mode 100644 index 000000000..7ce1ad670 --- /dev/null +++ b/src/net/java/sip/communicator/plugin/accountinfo/AccountInfoMenuItemComponent.java @@ -0,0 +1,119 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. See terms of license at gnu.org. + */ +package net.java.sip.communicator.plugin.accountinfo; + +import java.awt.event.*; + +import javax.swing.*; + +import net.java.sip.communicator.plugin.desktoputil.*; +import net.java.sip.communicator.service.gui.*; +import net.java.sip.communicator.service.protocol.*; + +/** + * Implements PluginComponent for the "Account Info" menu item. + * + * @author Marin Dzhigarov + */ +public class AccountInfoMenuItemComponent + extends AbstractPluginComponent +{ + /** + * The "Account Info" menu item. + */ + JMenuItem accountInfoMenuItem; + + /** + * The dialog that appears when "Account Info" menu item is clicked. + */ + static final SIPCommDialog dialog = new SIPCommDialog() { + /** + * Serial version UID. + */ + private static final long serialVersionUID = 1L; + + /** + * Presses programmatically the cancel button, when Esc key is pressed. + * + * @param isEscaped indicates if the Esc button was pressed on close + */ + protected void close(boolean isEscaped) + { + this.setVisible(false); + } + }; + + /** + * The main panel containing account information. + */ + static final AccountInfoPanel accountInfoPanel = new AccountInfoPanel(); + + /** + * Initializes a new "Account Info" menu item. + * + * @param container the container of the update menu component + */ + public AccountInfoMenuItemComponent(Container container, + PluginComponentFactory parentFactory) + { + super(container, parentFactory); + + AccountInfoActivator.bundleContext.addServiceListener(accountInfoPanel); + + dialog.setPreferredSize(new java.awt.Dimension(600, 400)); + dialog.setTitle(Resources.getString("plugin.accountinfo.TITLE")); + dialog.add(accountInfoPanel); + } + + public void setCurrentAccountID(AccountID accountID) + { + accountInfoMenuItem.setEnabled( + accountID != null && accountID.isEnabled()); + accountInfoPanel.getAccountsComboBox().setSelectedItem( + accountInfoPanel.getAccountsTable().get(accountID)); + } + + /** + * Gets the UI Component of this PluginComponent. + * + * @return the UI Component of this PluginComponent + * @see PluginComponent#getComponent() + */ + public Object getComponent() + { + if(accountInfoMenuItem == null) + { + accountInfoMenuItem + = new JMenuItem( + Resources.getString("plugin.accountinfo.TITLE")); + accountInfoMenuItem.setIcon( + Resources.getImage( + "plugin.contactinfo.CONTACT_INFO_ICON")); + accountInfoMenuItem.addActionListener( + new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + dialog.setVisible(true); + accountInfoPanel.setVisible(true); + } + }); + } + return accountInfoMenuItem; + } + + /** + * Gets the name of this PluginComponent. + * + * @return the name of this PluginComponent + * @see PluginComponent#getName() + */ + public String getName() + { + return + Resources.getString("plugin.accountinfo.TITLE"); + } +} diff --git a/src/net/java/sip/communicator/plugin/accountinfo/AccountInfoPanel.java b/src/net/java/sip/communicator/plugin/accountinfo/AccountInfoPanel.java index 99d8c409e..4e052c5f1 100644 --- a/src/net/java/sip/communicator/plugin/accountinfo/AccountInfoPanel.java +++ b/src/net/java/sip/communicator/plugin/accountinfo/AccountInfoPanel.java @@ -6,6 +6,7 @@ package net.java.sip.communicator.plugin.accountinfo; import java.awt.*; +import java.awt.event.*; import java.util.*; import javax.swing.*; @@ -17,13 +18,15 @@ import org.osgi.framework.*; /** - * A GUI plug-in for SIP Communicator that will allow users to set cross + * A GUI plug-in for Jitsi that will allow users to set cross * protocol account information. * * @author Adam Goldstein + * @author Marin Dzhigarov */ public class AccountInfoPanel extends TransparentPanel + implements ServiceListener { /** * Serial version UID. @@ -31,25 +34,86 @@ public class AccountInfoPanel private static final long serialVersionUID = 0L; /** - * The right side of the AccountInfo frame that contains protocol specific - * account details. + * The panel that contains the currently active AccountDetailsPanel */ - private AccountDetailsPanel detailsPanel; + private final JPanel centerPanel = + new TransparentPanel(new BorderLayout(10, 10)); - private final Map accountsTable = - new Hashtable(); + /** + * The currently active AccountDetailsPanel + */ + private AccountDetailsPanel currentDetailsPanel; + + /** + * Combo box that is used for switching between accounts. + */ + private final JComboBox accountsComboBox; /** - * Constructs a frame with an AccuontInfoAccountPanel to display all - * registered accounts on the left, and an information interface, - * AccountDetailsPanel, on the right. + * Instances of the AccountDetailsPanel are created for every + * registered AccountID. All such pairs are stored in + * this map. + */ + private final Map + accountsTable = + new HashMap(); + + /** + * Creates an instance of AccountInfoPanel that contains combo box + * component with active user accounts and AccountDetailsPanel to + * display and edit account information. */ public AccountInfoPanel() { - super(new BorderLayout()); + setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + + accountsComboBox = new JComboBox(); + accountsComboBox.addItemListener(new ItemListener() + { + @Override + public void itemStateChanged(ItemEvent e) + { + if (e.getStateChange() == ItemEvent.SELECTED) + { + AccountDetailsPanel panel = + (AccountDetailsPanel) e.getItem(); + panel.setOpaque(false); + centerPanel.removeAll(); + centerPanel.add(panel, BorderLayout.CENTER); + centerPanel.revalidate(); + centerPanel.repaint(); + currentDetailsPanel = panel; + } + } + }); + + init(); + + centerPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + ComboBoxRenderer renderer = new ComboBoxRenderer(); + accountsComboBox.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); + accountsComboBox.setRenderer(renderer); + + JLabel comboLabel = new JLabel( + Resources.getString( + "plugin.accountinfo.SELECT_ACCOUNT")); + comboLabel.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); - JTabbedPane accountsTabbedPane = new SIPCommTabbedPane(); + JPanel comboBoxPanel = new TransparentPanel(); + comboBoxPanel.setLayout(new BoxLayout(comboBoxPanel, BoxLayout.X_AXIS)); + comboBoxPanel.setBorder( + BorderFactory.createEmptyBorder(10, 10, 10, 10)); + comboBoxPanel.add(comboLabel); + comboBoxPanel.add(accountsComboBox); + + add(comboBoxPanel); + add(centerPanel); + } + + private void init() + { for (ProtocolProviderFactory providerFactory : AccountInfoActivator .getProtocolProviderFactories().values()) { @@ -63,22 +127,56 @@ public AccountInfoPanel() { serRef = providerFactory.getProviderForAccount(accountID); - protocolProvider = (ProtocolProviderService) AccountInfoActivator + protocolProvider = (ProtocolProviderService)AccountInfoActivator .bundleContext.getService(serRef); - detailsPanel = new AccountDetailsPanel(protocolProvider); + currentDetailsPanel = new AccountDetailsPanel(protocolProvider); + + accountsTable.put( + protocolProvider.getAccountID(), currentDetailsPanel); - accountsTable.put(protocolProvider, detailsPanel); + accountsComboBox.addItem(currentDetailsPanel); protocolProvider.addRegistrationStateChangeListener( new RegistrationStateChangeListenerImpl()); - - accountsTabbedPane.addTab( - accountID.getUserID(), detailsPanel); } } + } + + /** + * A custom renderer to display properly AccountDetailsPanel + * in a combo box. + */ + private class ComboBoxRenderer extends DefaultListCellRenderer + { + /** + * Serial version UID. + */ + private static final long serialVersionUID = 0L; - this.add(accountsTabbedPane, BorderLayout.CENTER); + @Override + public Component getListCellRendererComponent( + JList list, Object value, int index, + boolean isSelected, boolean hasFocus) + { + JLabel renderer + = (JLabel) super.getListCellRendererComponent( + list, value, index, isSelected, hasFocus); + + if (value != null) + { + AccountDetailsPanel panel = (AccountDetailsPanel) value; + + renderer.setText( + panel.protocolProvider.getAccountID().getUserID()); + ImageIcon protocolIcon = + new ImageIcon(panel.protocolProvider.getProtocolIcon(). + getIcon((ProtocolIcon.ICON_SIZE_16x16))); + renderer.setIcon(protocolIcon); + } + + return renderer; + } } private class RegistrationStateChangeListenerImpl @@ -88,19 +186,117 @@ public void registrationStateChanged(RegistrationStateChangeEvent evt) { ProtocolProviderService protocolProvider = evt.getProvider(); - if (protocolProvider.getOperationSet( - OperationSetServerStoredAccountInfo.class) != null - && evt.getNewState() == RegistrationState.REGISTERED) + if (evt.getNewState() == RegistrationState.REGISTERED) { - if (accountsTable.containsKey(protocolProvider)) + if (accountsTable.containsKey(protocolProvider.getAccountID())) { AccountDetailsPanel detailsPanel - = accountsTable.get(protocolProvider); + = accountsTable.get(protocolProvider.getAccountID()); + detailsPanel.loadDetails(); + } + else + { + AccountDetailsPanel panel = + new AccountDetailsPanel(protocolProvider); + accountsTable.put(protocolProvider.getAccountID(), panel); + accountsComboBox.addItem(panel); + } + } + else if (evt.getNewState() == RegistrationState.UNREGISTERING) + { + AccountDetailsPanel panel + = accountsTable.get(protocolProvider.getAccountID()); + if (panel != null) + { + accountsTable.remove(protocolProvider.getAccountID()); + accountsComboBox.removeItem(panel); + if (currentDetailsPanel == panel) + { + currentDetailsPanel = null; + centerPanel.removeAll(); + centerPanel.revalidate(); + centerPanel.repaint(); + } + } + } + } + } + + /** + * Handles registration and unregistration of + * ProtocolProviderService + * + * @param event + */ + @Override + public void serviceChanged(ServiceEvent event) + { + // Get the service from the event. + Object service + = AccountInfoActivator.bundleContext.getService( + event.getServiceReference()); - if(!detailsPanel.isDataLoaded()) - detailsPanel.loadDetails(); + // We are not interested in any services + // other than ProtocolProviderService + if (!(service instanceof ProtocolProviderService)) + return; + + ProtocolProviderService protocolProvider = + (ProtocolProviderService) service; + + // If a new protocol provider is registered we to add new + // AccountDetailsPanel to the combo box containing active accounts. + if (event.getType() == ServiceEvent.REGISTERED) + { + if (accountsTable.get(protocolProvider.getAccountID()) == null) + { + AccountDetailsPanel panel = + new AccountDetailsPanel(protocolProvider); + accountsTable.put(protocolProvider.getAccountID(), panel); + accountsComboBox.addItem(panel); + protocolProvider.addRegistrationStateChangeListener( + new RegistrationStateChangeListenerImpl()); + } + } + // If the protocol provider is being unregistered we have to remove + // a AccountDetailsPanel from the combo box containing active accounts. + else if (event.getType() == ServiceEvent.UNREGISTERING) + { + AccountDetailsPanel panel + = accountsTable.get(protocolProvider.getAccountID()); + if (panel != null) + { + accountsTable.remove(protocolProvider.getAccountID()); + accountsComboBox.removeItem(panel); + if (currentDetailsPanel == panel) + { + currentDetailsPanel = null; + centerPanel.removeAll(); + centerPanel.revalidate(); + centerPanel.repaint(); } } } } + + /** + * Returns the combo box that switches between account detail panels. + * + * @return The combo box that switches between account detail panels. + */ + public JComboBox getAccountsComboBox() + { + return accountsComboBox; + } + + /** + * Returns mapping between registered AccountIDs and their respective + * AccountDetailsPanel that contains all the details for the account. + * + * @return mapping between registered AccountIDs and AccountDetailsPanel. + */ + public Map getAccountsTable() + { + return accountsTable; + } } diff --git a/src/net/java/sip/communicator/plugin/accountinfo/accountinfo.manifest.mf b/src/net/java/sip/communicator/plugin/accountinfo/accountinfo.manifest.mf index 3426abd61..fca236bb5 100644 --- a/src/net/java/sip/communicator/plugin/accountinfo/accountinfo.manifest.mf +++ b/src/net/java/sip/communicator/plugin/accountinfo/accountinfo.manifest.mf @@ -5,27 +5,19 @@ Bundle-Vendor: jitsi.org Bundle-Version: 0.0.1 System-Bundle: yes Import-Package: org.osgi.framework, - net.java.sip.communicator.service.browserlauncher, - net.java.sip.communicator.service.contactlist, - net.java.sip.communicator.service.contactlist.event, net.java.sip.communicator.service.gui, - net.java.sip.communicator.service.gui.event, + net.java.sip.communicator.service.globaldisplaydetails, net.java.sip.communicator.service.protocol, net.java.sip.communicator.service.protocol.event, - org.jitsi.service.resources, net.java.sip.communicator.service.resources, + org.jitsi.service.resources, + org.jitsi.util, + net.java.sip.communicator.service.resources, net.java.sip.communicator.util, + net.java.sip.communicator.util.skin, net.java.sip.communicator.plugin.desktoputil, + net.java.sip.communicator.plugin.desktoputil.presence.avatar, javax.swing, javax.swing.event, - javax.swing.table, + javax.swing.border, javax.swing.text, - javax.swing.text.html, - javax.accessibility, - javax.swing.plaf, - javax.swing.plaf.metal, - javax.swing.plaf.basic, - javax.imageio, - javax.swing.filechooser, - javax.swing.tree, - javax.swing.undo, - javax.swing.border + javax.imageio diff --git a/src/net/java/sip/communicator/plugin/desktoputil/DesktopUtilActivator.java b/src/net/java/sip/communicator/plugin/desktoputil/DesktopUtilActivator.java index de92b2814..23bb10d53 100644 --- a/src/net/java/sip/communicator/plugin/desktoputil/DesktopUtilActivator.java +++ b/src/net/java/sip/communicator/plugin/desktoputil/DesktopUtilActivator.java @@ -12,10 +12,14 @@ import net.java.sip.communicator.service.credentialsstorage.*; import net.java.sip.communicator.service.gui.*; import net.java.sip.communicator.service.keybindings.*; +import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.service.resources.*; import net.java.sip.communicator.util.*; +import org.jitsi.service.audionotifier.*; import org.jitsi.service.configuration.*; +import org.jitsi.service.fileaccess.*; +import org.jitsi.service.neomedia.*; import org.jitsi.service.resources.*; import org.osgi.framework.*; @@ -40,6 +44,14 @@ public class DesktopUtilActivator private static UIService uiService; + private static AccountManager accountManager; + + private static FileAccessService fileAccessService; + + private static MediaService mediaService; + + private static AudioNotifierService audioNotifierService; + static BundleContext bundleContext; /** @@ -247,4 +259,69 @@ public VerifyCertificateDialog createDialog( { return new VerifyCertificateDialogImpl(certs, title, message); } + + /** + * Returns the AccountManager obtained from the bundle context. + * @return the AccountManager obtained from the bundle context + */ + public static AccountManager getAccountManager() + { + if(accountManager == null) + { + accountManager + = ServiceUtils.getService(bundleContext, AccountManager.class); + } + return accountManager; + } + + /** + * Returns the FileAccessService obtained from the bundle context. + * + * @return the FileAccessService obtained from the bundle context + */ + public static FileAccessService getFileAccessService() + { + if (fileAccessService == null) + { + fileAccessService + = ServiceUtils.getService( + bundleContext, + FileAccessService.class); + } + return fileAccessService; + } + + /** + * Returns an instance of the MediaService obtained from the + * bundle context. + * @return an instance of the MediaService obtained from the + * bundle context + */ + public static MediaService getMediaService() + { + if (mediaService == null) + { + mediaService + = ServiceUtils.getService(bundleContext, MediaService.class); + } + return mediaService; + } + + /** + * Returns the AudioNotifierService obtained from the bundle + * context. + * @return the AudioNotifierService obtained from the bundle + * context + */ + public static AudioNotifierService getAudioNotifier() + { + if (audioNotifierService == null) + { + audioNotifierService + = ServiceUtils.getService( + bundleContext, + AudioNotifierService.class); + } + return audioNotifierService; + } } \ No newline at end of file diff --git a/src/net/java/sip/communicator/plugin/desktoputil/FramedImage.java b/src/net/java/sip/communicator/plugin/desktoputil/FramedImage.java index a60df4881..de855578b 100644 --- a/src/net/java/sip/communicator/plugin/desktoputil/FramedImage.java +++ b/src/net/java/sip/communicator/plugin/desktoputil/FramedImage.java @@ -142,14 +142,17 @@ public void paintComponent(Graphics g) null); } - int frameWidth = frameImage.getWidth(this); - int frameHeight = frameImage.getHeight(this); - if ((frameWidth != -1) && (frameHeight != -1)) - g.drawImage( - frameImage, - width / 2 - frameWidth / 2, - height / 2 - frameHeight / 2, - null); + if (frameImage != null) + { + int frameWidth = frameImage.getWidth(this); + int frameHeight = frameImage.getHeight(this); + if ((frameWidth != -1) && (frameHeight != -1)) + g.drawImage( + frameImage, + width / 2 - frameWidth / 2, + height / 2 - frameHeight / 2, + null); + } } /** @@ -157,14 +160,21 @@ public void paintComponent(Graphics g) */ public void loadSkin() { - this.frameImage - = ImageUtils - .scaleImageWithinBounds( - DesktopUtilActivator - .getResources() - .getImage("service.gui.USER_PHOTO_FRAME").getImage(), - width, - height); + ImageIcon frameIcon = DesktopUtilActivator + .getResources() + .getImage("service.gui.USER_PHOTO_FRAME"); + + // Frame image will be drawn only if's bigger or equal to the underlying + // image. We would like to avoid pixelated results! + if (frameIcon.getIconWidth() >= width + && frameIcon.getIconHeight() >= frameIcon.getIconHeight()) + { + this.frameImage + = ImageUtils + .scaleImageWithinBounds(frameIcon.getImage(), + width, + height); + } } /** diff --git a/src/net/java/sip/communicator/plugin/desktoputil/FramedImageWithMenu.java b/src/net/java/sip/communicator/plugin/desktoputil/FramedImageWithMenu.java index f50ca68f2..4262e3bbf 100644 --- a/src/net/java/sip/communicator/plugin/desktoputil/FramedImageWithMenu.java +++ b/src/net/java/sip/communicator/plugin/desktoputil/FramedImageWithMenu.java @@ -34,11 +34,6 @@ public class FramedImageWithMenu */ private JPopupMenu popupMenu; - /** - * The parent frame. - */ - private JFrame mainFrame; - /** * Should we currently draw overlay. */ @@ -62,14 +57,12 @@ public class FramedImageWithMenu * @param height height of component. */ public FramedImageWithMenu( - JFrame mainFrame, ImageIcon imageIcon, int width, int height) { super(imageIcon, width, height); - this.mainFrame = mainFrame; this.addMouseListener(this); } @@ -189,9 +182,9 @@ private void showDialog(MouseEvent e, boolean show) if (show) { Point imageLoc = this.getLocationOnScreen(); - Point rootPaneLoc = mainFrame.getRootPane().getLocationOnScreen(); + Point rootPaneLoc = this.getRootPane().getLocationOnScreen(); - this.popupMenu.setSize(mainFrame.getRootPane().getWidth(), + this.popupMenu.setSize(this.getRootPane().getWidth(), this.popupMenu.getHeight()); this.popupMenu.show(this, (rootPaneLoc.x - imageLoc.x), @@ -206,7 +199,7 @@ private void showDialog(MouseEvent e, boolean show) public void mouseEntered(MouseEvent e) { - if (this.drawOverlay) + if (this.drawOverlay || !this.isEnabled()) return; this.drawOverlay = true; @@ -222,7 +215,7 @@ public void mouseEntered(MouseEvent e) public void mouseExited(MouseEvent e) { // Remove overlay only if the dialog isn't visible - if (!popupMenu.isVisible()) + if (!popupMenu.isVisible() && this.isEnabled()) { this.drawOverlay = false; this.repaint(); @@ -231,7 +224,8 @@ public void mouseExited(MouseEvent e) public void mouseReleased(MouseEvent e) { - showDialog(e, !popupMenu.isVisible()); + if (this.isEnabled()) + showDialog(e, !popupMenu.isVisible()); } /** diff --git a/src/net/java/sip/communicator/plugin/desktoputil/desktoputil.manifest.mf b/src/net/java/sip/communicator/plugin/desktoputil/desktoputil.manifest.mf index 56edae3da..4b91013e6 100644 --- a/src/net/java/sip/communicator/plugin/desktoputil/desktoputil.manifest.mf +++ b/src/net/java/sip/communicator/plugin/desktoputil/desktoputil.manifest.mf @@ -30,6 +30,7 @@ Import-Package: com.sun.awt, javax.xml.transform.dom, javax.xml.transform.stream, net.java.sip.communicator.util, + net.java.sip.communicator.util.account, net.java.sip.communicator.util.skin, net.java.sip.communicator.util.wizard, net.java.sip.communicator.service.certificate, @@ -64,5 +65,6 @@ Export-Package: net.java.sip.communicator.plugin.desktoputil, net.java.sip.communicator.plugin.desktoputil.event, net.java.sip.communicator.plugin.desktoputil.plaf, net.java.sip.communicator.plugin.desktoputil.presence, + net.java.sip.communicator.plugin.desktoputil.presence.avatar, net.java.sip.communicator.plugin.desktoputil.transparent, net.java.sip.communicator.plugin.desktoputil.wizard diff --git a/src/net/java/sip/communicator/impl/gui/main/presence/avatar/AvatarStackManager.java b/src/net/java/sip/communicator/plugin/desktoputil/presence/avatar/AvatarStackManager.java similarity index 82% rename from src/net/java/sip/communicator/impl/gui/main/presence/avatar/AvatarStackManager.java rename to src/net/java/sip/communicator/plugin/desktoputil/presence/avatar/AvatarStackManager.java index 948716e94..3a81a245f 100644 --- a/src/net/java/sip/communicator/impl/gui/main/presence/avatar/AvatarStackManager.java +++ b/src/net/java/sip/communicator/plugin/desktoputil/presence/avatar/AvatarStackManager.java @@ -4,18 +4,18 @@ * Distributable under LGPL license. * See terms of license at gnu.org. */ -package net.java.sip.communicator.impl.gui.main.presence.avatar; +package net.java.sip.communicator.plugin.desktoputil.presence.avatar; import java.awt.image.*; import java.io.*; import javax.imageio.*; -import org.jitsi.service.fileaccess.*; - -import net.java.sip.communicator.impl.gui.*; +import net.java.sip.communicator.plugin.desktoputil.*; import net.java.sip.communicator.util.*; +import org.jitsi.service.fileaccess.*; + /** * Take cares of storing(deleting, moving) images with the given indexes. */ @@ -44,8 +44,9 @@ public static void deleteImage(int index) try { File imageFile - = GuiActivator.getFileAccessService().getPrivatePersistentFile( - fileName, FileCategory.CACHE); + = DesktopUtilActivator.getFileAccessService() + .getPrivatePersistentFile( + fileName); if (imageFile.exists() && !imageFile.delete()) logger.error("Failed to delete stored image at index " + index); @@ -71,8 +72,9 @@ public static BufferedImage loadImage(int index) String imagePath = STORE_DIR + index + ".png"; imageFile - = GuiActivator.getFileAccessService().getPrivatePersistentFile( - imagePath, FileCategory.CACHE); + = DesktopUtilActivator.getFileAccessService(). + getPrivatePersistentFile( + imagePath); } catch (Exception e) { @@ -107,14 +109,12 @@ private static void moveImage(int oldIndex, int newIndex) try { - FileAccessService fas = GuiActivator.getFileAccessService(); - File oldFile = fas.getPrivatePersistentFile(oldImagePath, - FileCategory.CACHE); + FileAccessService fas = DesktopUtilActivator.getFileAccessService(); + File oldFile = fas.getPrivatePersistentFile(oldImagePath); if (oldFile.exists()) { - File newFile = fas.getPrivatePersistentFile(newImagePath, - FileCategory.CACHE); + File newFile = fas.getPrivatePersistentFile(newImagePath); oldFile.renameTo(newFile); } @@ -148,15 +148,13 @@ public static void storeImage(BufferedImage image, int index) try { - FileAccessService fas = GuiActivator.getFileAccessService(); - File storeDir = fas.getPrivatePersistentDirectory(STORE_DIR, - FileCategory.CACHE); + FileAccessService fas = DesktopUtilActivator.getFileAccessService(); + File storeDir = fas.getPrivatePersistentDirectory(STORE_DIR); // if dir doesn't exist create it storeDir.mkdirs(); - File file = fas.getPrivatePersistentFile(imagePath, - FileCategory.CACHE); + File file = fas.getPrivatePersistentFile(imagePath); ImageIO.write(image, "png", file); } diff --git a/src/net/java/sip/communicator/impl/gui/main/presence/avatar/SelectAvatarMenu.java b/src/net/java/sip/communicator/plugin/desktoputil/presence/avatar/SelectAvatarMenu.java similarity index 67% rename from src/net/java/sip/communicator/impl/gui/main/presence/avatar/SelectAvatarMenu.java rename to src/net/java/sip/communicator/plugin/desktoputil/presence/avatar/SelectAvatarMenu.java index 01c70aaeb..7b00fcca8 100644 --- a/src/net/java/sip/communicator/impl/gui/main/presence/avatar/SelectAvatarMenu.java +++ b/src/net/java/sip/communicator/plugin/desktoputil/presence/avatar/SelectAvatarMenu.java @@ -4,18 +4,20 @@ * Distributable under LGPL license. * See terms of license at gnu.org. */ -package net.java.sip.communicator.impl.gui.main.presence.avatar; +package net.java.sip.communicator.plugin.desktoputil.presence.avatar; import java.awt.*; import java.awt.event.*; import java.awt.image.*; +import java.util.*; import javax.swing.*; -import net.java.sip.communicator.impl.gui.*; -import net.java.sip.communicator.impl.gui.main.presence.avatar.imagepicker.*; import net.java.sip.communicator.plugin.desktoputil.*; +import net.java.sip.communicator.plugin.desktoputil.presence.avatar.imagepicker.*; import net.java.sip.communicator.service.protocol.*; +import net.java.sip.communicator.service.protocol.ServerStoredDetails.GenericDetail; +import net.java.sip.communicator.service.protocol.ServerStoredDetails.ImageDetail; import net.java.sip.communicator.util.*; import net.java.sip.communicator.util.account.*; @@ -85,6 +87,12 @@ public class SelectAvatarMenu */ private FramedImageWithMenu avatarImage; + /** + * The AccountID that we want to select avatar for. Could be null if + * we want to select a global avatar. + */ + private AccountID accountID; + /** * Creates the dialog. * @param avatarImage the button that will trigger this menu. @@ -98,6 +106,11 @@ public SelectAvatarMenu(FramedImageWithMenu avatarImage) this.pack(); } + public void setAccountID(AccountID accountID) + { + this.accountID = accountID; + } + /** * Init visible components. */ @@ -108,7 +121,7 @@ private void init() panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); // Title label - JLabel titleLabel = new JLabel(GuiActivator.getResources() + JLabel titleLabel = new JLabel(DesktopUtilActivator.getResources() .getI18NString("service.gui.avatar.RECENT_ICONS")); titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD)); @@ -146,17 +159,17 @@ private void init() Color linkColor = new JMenuItem().getForeground(); addActionButton(buttonsPanel, this, - GuiActivator.getResources().getI18NString( + DesktopUtilActivator.getResources().getI18NString( "service.gui.avatar.CHOOSE_ICON"), CHOSE_BUTTON_NAME, linkColor); addActionButton(buttonsPanel, this, - GuiActivator.getResources().getI18NString( + DesktopUtilActivator.getResources().getI18NString( "service.gui.avatar.REMOVE_ICON"), REMOVE_BUTTON_NAME, linkColor); addActionButton(buttonsPanel, this, - GuiActivator.getResources().getI18NString( + DesktopUtilActivator.getResources().getI18NString( "service.gui.avatar.CLEAR_RECENT"), CLEAR_BUTTON_NAME, linkColor); @@ -267,11 +280,11 @@ private void setNewImage(final BufferedImage image) public void run() { AccountManager accountManager - = GuiActivator.getAccountManager(); + = DesktopUtilActivator.getAccountManager(); - for(AccountID accountID : accountManager.getStoredAccounts()) + for (AccountID accountID : accountManager.getStoredAccounts()) { - if(accountManager.isAccountLoaded(accountID)) + if (accountManager.isAccountLoaded(accountID)) { ProtocolProviderService protocolProvider = AccountUtils.getRegisteredProviderForAccount( @@ -280,27 +293,78 @@ public void run() if(protocolProvider != null && protocolProvider.isRegistered()) { - OperationSetAvatar opSetAvatar - = protocolProvider - .getOperationSet(OperationSetAvatar.class); - - if(opSetAvatar != null) + // If account id is set this means that we want to + // edit our current account image, not the global + // avatar. Hence, we might not want to save this + // account image on the server yet. For example: in + // the account info plugin the user might set a new + // avatar and then click the cancel button. + if (SelectAvatarMenu.this.accountID != null) { - byte[] imageByte = null; - // Sets new avatar if not null. Otherwise, the - // opSetAvatar.setAvatar(null) will removes the - // current one. - if(image != null) - { - imageByte = ImageUtils.toByteArray(image); - } - try + if (accountID.equals( + SelectAvatarMenu.this.accountID)) { - opSetAvatar.setAvatar(imageByte); + OperationSetServerStoredAccountInfo opSet = + protocolProvider.getOperationSet( + OperationSetServerStoredAccountInfo.class); + if (opSet != null) + { + byte[] imageByte = null; + if (image != null) + { + imageByte = + ImageUtils.toByteArray(image); + } + avatarImage.setImageIcon(imageByte); + ImageDetail newDetail = + new ImageDetail( + "avatar", imageByte); + + Iterator oldDetail = + opSet.getDetails(ImageDetail.class); + try + { + if (oldDetail.hasNext()) + { + opSet.replaceDetail( + oldDetail.next(), + newDetail); + } + else + opSet.addDetail(newDetail); + } + catch (Throwable t) + { + logger.error( + "Error setting image", t); + } + } } - catch(Throwable t) + } + else + { + OperationSetAvatar opSetAvatar + = protocolProvider + .getOperationSet(OperationSetAvatar.class); + + if(opSetAvatar != null) { - logger.error("Error setting image", t); + byte[] imageByte = null; + // Sets new avatar if not null. Otherwise, the + // opSetAvatar.setAvatar(null) will removes the + // current one. + if(image != null) + { + imageByte = ImageUtils.toByteArray(image); + } + try + { + opSetAvatar.setAvatar(imageByte); + } + catch(Throwable t) + { + logger.error("Error setting image", t); + } } } } diff --git a/src/net/java/sip/communicator/impl/gui/main/presence/avatar/imagepicker/EditPanel.java b/src/net/java/sip/communicator/plugin/desktoputil/presence/avatar/imagepicker/EditPanel.java similarity index 94% rename from src/net/java/sip/communicator/impl/gui/main/presence/avatar/imagepicker/EditPanel.java rename to src/net/java/sip/communicator/plugin/desktoputil/presence/avatar/imagepicker/EditPanel.java index b8273c891..aff02809b 100644 --- a/src/net/java/sip/communicator/impl/gui/main/presence/avatar/imagepicker/EditPanel.java +++ b/src/net/java/sip/communicator/plugin/desktoputil/presence/avatar/imagepicker/EditPanel.java @@ -4,7 +4,7 @@ * Distributable under LGPL license. * See terms of license at gnu.org. */ -package net.java.sip.communicator.impl.gui.main.presence.avatar.imagepicker; +package net.java.sip.communicator.plugin.desktoputil.presence.avatar.imagepicker; import java.awt.*; import java.awt.event.*; @@ -13,7 +13,6 @@ import javax.swing.*; import javax.swing.event.*; -import net.java.sip.communicator.impl.gui.*; import net.java.sip.communicator.plugin.desktoputil.*; /** @@ -53,15 +52,15 @@ public EditPanel(int clippingZoneWidth, int clippingZoneHeight) this.clippingZoneWidth = clippingZoneWidth; this.clippingZoneHeight = clippingZoneHeight; - this.zoomOut = new JButton(GuiActivator.getResources() + this.zoomOut = new JButton(DesktopUtilActivator.getResources() .getImage("service.gui.buttons.ZOOM_OUT")); this.zoomOut.addActionListener(this); - this.zoomIn = new JButton(GuiActivator.getResources() + this.zoomIn = new JButton(DesktopUtilActivator.getResources() .getImage("service.gui.buttons.ZOOM_IN")); this.zoomIn.addActionListener(this); - this.reset = new JButton(GuiActivator.getResources() + this.reset = new JButton(DesktopUtilActivator.getResources() .getImage("service.gui.buttons.RESET")); - this.reset.setToolTipText(GuiActivator.getResources() + this.reset.setToolTipText(DesktopUtilActivator.getResources() .getI18NString("service.gui.avatar.imagepicker.RESET")); this.reset.addActionListener(this); @@ -69,7 +68,7 @@ public EditPanel(int clippingZoneWidth, int clippingZoneHeight) clippingZoneWidth); imageSizeSlider.addChangeListener(this); imageSizeSlider.setOpaque(false); - imageSizeSlider.setToolTipText(GuiActivator.getResources() + imageSizeSlider.setToolTipText(DesktopUtilActivator.getResources() .getI18NString("service.gui.avatar.imagepicker.IMAGE_SIZE")); TransparentPanel sliderPanel = new TransparentPanel(); diff --git a/src/net/java/sip/communicator/impl/gui/main/presence/avatar/imagepicker/ImageClipper.java b/src/net/java/sip/communicator/plugin/desktoputil/presence/avatar/imagepicker/ImageClipper.java similarity index 99% rename from src/net/java/sip/communicator/impl/gui/main/presence/avatar/imagepicker/ImageClipper.java rename to src/net/java/sip/communicator/plugin/desktoputil/presence/avatar/imagepicker/ImageClipper.java index 5f42a262c..853b9060f 100644 --- a/src/net/java/sip/communicator/impl/gui/main/presence/avatar/imagepicker/ImageClipper.java +++ b/src/net/java/sip/communicator/plugin/desktoputil/presence/avatar/imagepicker/ImageClipper.java @@ -4,7 +4,7 @@ * Distributable under LGPL license. * See terms of license at gnu.org. */ -package net.java.sip.communicator.impl.gui.main.presence.avatar.imagepicker; +package net.java.sip.communicator.plugin.desktoputil.presence.avatar.imagepicker; import java.awt.*; import java.awt.event.*; diff --git a/src/net/java/sip/communicator/impl/gui/main/presence/avatar/imagepicker/ImagePickerDialog.java b/src/net/java/sip/communicator/plugin/desktoputil/presence/avatar/imagepicker/ImagePickerDialog.java similarity index 90% rename from src/net/java/sip/communicator/impl/gui/main/presence/avatar/imagepicker/ImagePickerDialog.java rename to src/net/java/sip/communicator/plugin/desktoputil/presence/avatar/imagepicker/ImagePickerDialog.java index 06e135b43..8a1b2f41d 100644 --- a/src/net/java/sip/communicator/impl/gui/main/presence/avatar/imagepicker/ImagePickerDialog.java +++ b/src/net/java/sip/communicator/plugin/desktoputil/presence/avatar/imagepicker/ImagePickerDialog.java @@ -4,7 +4,7 @@ * Distributable under LGPL license. * See terms of license at gnu.org. */ -package net.java.sip.communicator.impl.gui.main.presence.avatar.imagepicker; +package net.java.sip.communicator.plugin.desktoputil.presence.avatar.imagepicker; import java.awt.*; import java.awt.event.*; @@ -14,7 +14,6 @@ import javax.imageio.*; import javax.swing.*; -import net.java.sip.communicator.impl.gui.*; import net.java.sip.communicator.plugin.desktoputil.*; /** @@ -51,7 +50,7 @@ public ImagePickerDialog(int clipperZoneWidth, int clipperZoneHeight) */ private void initDialog() { - this.setTitle(GuiActivator.getResources() + this.setTitle(DesktopUtilActivator.getResources() .getI18NString("service.gui.avatar.imagepicker.IMAGE_PICKER")); this.setModal(true); this.setResizable(true); @@ -91,22 +90,22 @@ private void initComponents(int clipperZoneWidth, int clipperZoneHeight) this.editPanel = new EditPanel(clipperZoneWidth, clipperZoneHeight); // Buttons - this.okButton = new JButton(GuiActivator.getResources() + this.okButton = new JButton(DesktopUtilActivator.getResources() .getI18NString("service.gui.avatar.imagepicker.SET")); this.okButton.addActionListener(this); this.okButton.setName("okButton"); - this.cancelButton = new JButton(GuiActivator.getResources() + this.cancelButton = new JButton(DesktopUtilActivator.getResources() .getI18NString("service.gui.avatar.imagepicker.CANCEL")); this.cancelButton.addActionListener(this); this.cancelButton.setName("cancelButton"); - this.selectFileButton = new JButton(GuiActivator.getResources() + this.selectFileButton = new JButton(DesktopUtilActivator.getResources() .getI18NString("service.gui.avatar.imagepicker.CHOOSE_FILE")); this.selectFileButton.addActionListener(this); this.selectFileButton.setName("selectFileButton"); - this.webcamButton = new JButton(GuiActivator.getResources() + this.webcamButton = new JButton(DesktopUtilActivator.getResources() .getI18NString("service.gui.avatar.imagepicker.TAKE_PHOTO")); this.webcamButton.addActionListener(this); @@ -146,8 +145,9 @@ public void actionPerformed(ActionEvent e) else if (name.equals("selectFileButton")) { SipCommFileChooser chooser = GenericFileDialog.create( - GuiActivator.getUIService().getMainFrame(), - GuiActivator.getResources().getI18NString( + //GuiActivator.getUIService().getMainFrame(), + null, + DesktopUtilActivator.getResources().getI18NString( "service.gui.avatar.imagepicker.CHOOSE_FILE"), SipCommFileChooser.LOAD_FILE_OPERATION); @@ -217,7 +217,7 @@ public boolean accept(File f) @Override public String getDescription() { - return GuiActivator.getResources() + return DesktopUtilActivator.getResources() .getI18NString("service.gui.avatar.imagepicker.IMAGE_FILES"); } } diff --git a/src/net/java/sip/communicator/impl/gui/main/presence/avatar/imagepicker/WebcamDialog.java b/src/net/java/sip/communicator/plugin/desktoputil/presence/avatar/imagepicker/WebcamDialog.java similarity index 93% rename from src/net/java/sip/communicator/impl/gui/main/presence/avatar/imagepicker/WebcamDialog.java rename to src/net/java/sip/communicator/plugin/desktoputil/presence/avatar/imagepicker/WebcamDialog.java index a72f13d50..ed4a8baf1 100644 --- a/src/net/java/sip/communicator/impl/gui/main/presence/avatar/imagepicker/WebcamDialog.java +++ b/src/net/java/sip/communicator/plugin/desktoputil/presence/avatar/imagepicker/WebcamDialog.java @@ -4,7 +4,7 @@ * Distributable under LGPL license. * See terms of license at gnu.org. */ -package net.java.sip.communicator.impl.gui.main.presence.avatar.imagepicker; +package net.java.sip.communicator.plugin.desktoputil.presence.avatar.imagepicker; import java.awt.*; import java.awt.event.*; @@ -12,10 +12,8 @@ import javax.swing.*; -import net.java.sip.communicator.impl.gui.*; -import net.java.sip.communicator.util.*; import net.java.sip.communicator.plugin.desktoputil.*; -import net.java.sip.communicator.plugin.desktoputil.TransparentPanel; +import net.java.sip.communicator.util.*; import org.jitsi.service.audionotifier.*; import org.jitsi.service.neomedia.*; @@ -51,7 +49,7 @@ public class WebcamDialog public WebcamDialog(ImagePickerDialog parent) { super(false); - this.setTitle(GuiActivator.getResources() + this.setTitle(DesktopUtilActivator.getResources() .getI18NString("service.gui.avatar.imagepicker.TAKE_PHOTO")); this.setModal(true); @@ -66,13 +64,13 @@ public WebcamDialog(ImagePickerDialog parent) private void init() { this.grabSnapshot = new JButton(); - this.grabSnapshot.setText(GuiActivator.getResources() + this.grabSnapshot.setText(DesktopUtilActivator.getResources() .getI18NString("service.gui.avatar.imagepicker.CLICK")); this.grabSnapshot.setName("grab"); this.grabSnapshot.addActionListener(this); this.grabSnapshot.setEnabled(false); - JButton cancelButton = new JButton(GuiActivator.getResources() + JButton cancelButton = new JButton(DesktopUtilActivator.getResources() .getI18NString("service.gui.avatar.imagepicker.CANCEL")); cancelButton.setName("cancel"); cancelButton.addActionListener(this); @@ -120,7 +118,7 @@ private void init() private void initAccessWebcam() { //Call the method in the media service - MediaService mediaService = GuiActivator.getMediaService(); + MediaService mediaService = DesktopUtilActivator.getMediaService(); this.videoContainer = (Component) @@ -174,10 +172,10 @@ public byte[] getGrabbedImage() */ private void playSound() { - String soundKey = GuiActivator.getResources() + String soundKey = DesktopUtilActivator.getResources() .getSoundPath("WEBCAM_SNAPSHOT"); - SCAudioClip audio = GuiActivator.getAudioNotifier() + SCAudioClip audio = DesktopUtilActivator.getAudioNotifier() .createAudio(soundKey); audio.play(); diff --git a/src/net/java/sip/communicator/plugin/spellcheck/LanguageMenuBar.java b/src/net/java/sip/communicator/plugin/spellcheck/LanguageMenuBar.java index 2bdf423c5..a79b535b9 100644 --- a/src/net/java/sip/communicator/plugin/spellcheck/LanguageMenuBar.java +++ b/src/net/java/sip/communicator/plugin/spellcheck/LanguageMenuBar.java @@ -261,6 +261,11 @@ public void setCurrentContactGroup(MetaContactGroup metaGroup) } + public void setCurrentAccountID(AccountID account) + { + + } + private static ImageIcon getLocaleIcon(Parameters.Locale locale, boolean isAvailable) { diff --git a/src/net/java/sip/communicator/service/gui/AbstractPluginComponent.java b/src/net/java/sip/communicator/service/gui/AbstractPluginComponent.java index fd6ab6c13..c5ec1da30 100644 --- a/src/net/java/sip/communicator/service/gui/AbstractPluginComponent.java +++ b/src/net/java/sip/communicator/service/gui/AbstractPluginComponent.java @@ -106,6 +106,13 @@ public void setCurrentContactGroup(MetaContactGroup metaGroup) { } + /* + * Implements PluginComponent#setCurrentAccountID(AccountID). + */ + public void setCurrentAccountID(AccountID accountID) + { + } + /** * Returns the factory that has created the component. * @return the parent factory. diff --git a/src/net/java/sip/communicator/service/gui/Container.java b/src/net/java/sip/communicator/service/gui/Container.java index 03434fb5f..a493d86f4 100644 --- a/src/net/java/sip/communicator/service/gui/Container.java +++ b/src/net/java/sip/communicator/service/gui/Container.java @@ -71,6 +71,12 @@ public class Container public static final Container CONTAINER_CONTACT_RIGHT_BUTTON_MENU = new Container("CONTAINER_CONTACT_RIGHT_BUTTON_MENU"); + /** + * Accounts window "right button menu" over an account. + */ + public static final Container CONTAINER_ACCOUNT_RIGHT_BUTTON_MENU + = new Container("CONTAINER_ACCOUNT_RIGHT_BUTTON_MENU"); + /** * Main application window "right button menu" over a group container. */ diff --git a/src/net/java/sip/communicator/service/gui/PluginComponent.java b/src/net/java/sip/communicator/service/gui/PluginComponent.java index 3d039f317..f1984cf63 100644 --- a/src/net/java/sip/communicator/service/gui/PluginComponent.java +++ b/src/net/java/sip/communicator/service/gui/PluginComponent.java @@ -21,6 +21,11 @@ * implement the setCurrentContact and * setCurrentContactGroup methods. *

+ *

+ * All components interested in the current account that they're dealing + * with (i.g. the one selected in the account list for example), should + * implement the setCurrentAccountID method. + *

* Note that getComponent should return a valid AWT, SWT or Swing * control in order to appear properly in the GUI. * @@ -76,6 +81,16 @@ public interface PluginComponent */ public void setCurrentContactGroup(MetaContactGroup metaGroup); + /** + * Sets the current AccountID. Meant to be used by plugin components that are + * interested in the current AccountID. The current AccountID could be that + * of a currently selected account in the account list. It depends on the + * container, where this component is meant to be added. + * + * @param account the current account. + */ + public void setCurrentAccountID(AccountID accountID); + /** * Returns the factory that has created the component. * @return the parent factory. diff --git a/src/net/java/sip/communicator/service/protocol/AbstractOperationSetAvatar.java b/src/net/java/sip/communicator/service/protocol/AbstractOperationSetAvatar.java index f997238b0..7f1e192da 100644 --- a/src/net/java/sip/communicator/service/protocol/AbstractOperationSetAvatar.java +++ b/src/net/java/sip/communicator/service/protocol/AbstractOperationSetAvatar.java @@ -107,6 +107,7 @@ public void setAvatar(byte[] avatar) this.accountInfoOpSet.addDetail(newDetail); else this.accountInfoOpSet.replaceDetail(oldDetail, newDetail); + accountInfoOpSet.save(); } catch (OperationFailedException e) { logger.warn("Unable to set new avatar", e); diff --git a/src/net/java/sip/communicator/service/protocol/OperationSetServerStoredAccountInfo.java b/src/net/java/sip/communicator/service/protocol/OperationSetServerStoredAccountInfo.java index 3b859c667..82f281c17 100644 --- a/src/net/java/sip/communicator/service/protocol/OperationSetServerStoredAccountInfo.java +++ b/src/net/java/sip/communicator/service/protocol/OperationSetServerStoredAccountInfo.java @@ -115,6 +115,18 @@ public Iterator getDetails( public boolean isDetailClassSupported( Class detailClass); + /** + * Determines whether the underlying implementation supports edition + * of this detail class. + *

+ * @param detailClass the class whose edition we'd like to determine if it's + * possible + * @return true if the underlying implementation supports edition of this + * type of detail and false otherwise. + */ + public boolean isDetailClassEditable( + Class detailClass); + /** * The method returns the number of instances supported for a particular * detail type. Some protocols offer storing mutliple values for a @@ -128,7 +140,7 @@ public int getMaxDetailInstances( Class detailClass); /** - * Adds the specified detail to the list of details registered on-line + * Adds the specified detail to the list of details ready to be saved online * for this account. If such a detail already exists its max instance number * is consulted and if it allows it - a second instance is added or otherwise * and illegal argument exception is thrown. An IllegalArgumentException is @@ -139,11 +151,9 @@ public int getMaxDetailInstances( * @param detail the detail that we'd like registered on the server. *

* @throws IllegalArgumentException if such a detail already exists and its - * max instances number has been atteined or if the underlying + * max instances number has been attained or if the underlying * implementation does not support setting details of the corresponding * class. - * @throws OperationFailedException with code Network Failure if putting the - * new value online has failed * @throws java.lang.ArrayIndexOutOfBoundsException if the number of * instances currently registered by the application is already equal to the * maximum number of supported instances (@see getMaxDetailInstances()) @@ -154,15 +164,13 @@ public void addDetail(ServerStoredDetails.GenericDetail detail) ArrayIndexOutOfBoundsException; /** - * Removes the specified detail from the list of details stored online for - * this account. The method returns a boolean indicating if such a detail - * was found (and removed) or not. + * Removes the specified detail from the list of details ready to be saved + * online this account. The method returns a boolean indicating if such a + * detail was found (and removed) or not. *

* @param detail the detail to remove * @return true if the specified detail existed and was successfully removed * and false otherwise. - * @throws OperationFailedException with code Network Failure if removing the - * detail from the server has failed */ public boolean removeDetail(ServerStoredDetails.GenericDetail detail) throws OperationFailedException; @@ -186,6 +194,17 @@ public boolean replaceDetail( ServerStoredDetails.GenericDetail newDetailValue) throws ClassCastException, OperationFailedException; + /** + * Saves the list of details for this account that were ready to be stored + * online on the server. This method performs the actual saving of details + * online on the server and is supposed to be invoked after addDetail(), + * replaceDetail() and/or removeDetail(). + *

+ * @throws OperationFailedException with code Network Failure if putting the + * new values back online has failed. + */ + public void save() throws OperationFailedException; + /** * Registers a ServerStoredDetailsChangeListener with this operation set so * that it gets notifications of details change. diff --git a/src/net/java/sip/communicator/service/protocol/ServerStoredDetails.java b/src/net/java/sip/communicator/service/protocol/ServerStoredDetails.java index 4002e18a0..16b7e016a 100644 --- a/src/net/java/sip/communicator/service/protocol/ServerStoredDetails.java +++ b/src/net/java/sip/communicator/service/protocol/ServerStoredDetails.java @@ -306,6 +306,12 @@ public CountryDetail(Locale locale) { super("Country", locale); } + + public CountryDetail(String country) + { + super("Country", null); + value = country; + } } /** @@ -317,6 +323,11 @@ public WorkCountryDetail(Locale locale) { super(locale); } + + public WorkCountryDetail(String country) + { + super(country); + } } //-------------------------------- Language ------------------------------------ @@ -447,6 +458,43 @@ public URL getURL() { return (URL)getDetailValue(); } + + /** + * Compares two URLDetails according their name + * and URLs + * + * @param obj Object expected URLDetail otherwise return false + * @return true if this object has the same name and + * URL value as obj and false otherwise + */ + @Override + public boolean equals(Object obj) + { + if (!(obj instanceof URLDetail)) + return false; + + if (this == obj) + { + return true; + } + + URLDetail other = (URLDetail)obj; + boolean equalsDisplayName = + this.detailDisplayName != null + && other.getDetailDisplayName() != null + && this.detailDisplayName.equals(other.getDetailDisplayName()); + boolean equalValues = + this.value != null + && other.getDetailValue() != null + && this.value.equals(other.getDetailValue()); + + boolean bothNullValues = + this.value == null && other.value == null; + if (equalsDisplayName && (equalValues || bothNullValues)) + return true; + else + return false; + } } /** @@ -487,6 +535,44 @@ public byte[] getBytes() { return (byte[])getDetailValue(); } + + /** + * Compares two BinaryDetails according their DetailDisplayName + * and the result of invoking their getBytes() methods. + * + * @param obj Object expected BinaryDetail otherwise return false + * @return true if this object has the same display name and + * value as obj and false otherwise + */ + @Override + public boolean equals(Object obj) + { + if (!(obj instanceof BinaryDetail)) + return false; + + if (this == obj) + { + return true; + } + + BinaryDetail other = (BinaryDetail)obj; + boolean equalsDisplayName = + this.detailDisplayName != null + && other.getDetailDisplayName() != null + && this.detailDisplayName.equals(other.getDetailDisplayName()); + boolean equalsNotNull = + this.value != null + && other.getDetailValue() != null + && Arrays.equals(this.getBytes(), other.getBytes()); + boolean nullOrEmpty = + (this.value == null || this.getBytes().length == 0) + && (other.getDetailValue() == null + || other.getBytes().length == 0); + if (equalsDisplayName && (equalsNotNull || nullOrEmpty)) + return true; + else + return false; + } } /** @@ -641,6 +727,48 @@ public BirthDateDetail(Calendar date) { super("Birth Date", date); } + + /** + * Compares two BirthDateDetails according to their + * Calender's year, month and day. + * + * @param obj Object expected BirthDateDetail otherwise return false + * @return true if this object has the same value as + * obj and false otherwise + */ + @Override + public boolean equals(Object obj) + { + if(!(obj instanceof BirthDateDetail)) + return false; + + if(this == obj) + { + return true; + } + + BirthDateDetail other = (BirthDateDetail)obj; + + // both null dates + if (this.value == null && other.getDetailValue() == null) + return true; + + if (this.value != null && other.getDetailValue() != null) + { + boolean yearEquals = + ((Calendar)this.value).get(Calendar.YEAR) == + ((Calendar)other.value).get(Calendar.YEAR); + boolean monthEquals = + ((Calendar)this.value).get(Calendar.MONTH) == + ((Calendar)other.value).get(Calendar.MONTH); + boolean dayEquals = + ((Calendar)this.value).get(Calendar.DAY_OF_MONTH) == + ((Calendar)other.value).get(Calendar.DAY_OF_MONTH); + return yearEquals && monthEquals && dayEquals; + } + else + return false; + } } /** @@ -747,4 +875,27 @@ public boolean getBoolean() return ((Boolean)getDetailValue()).booleanValue(); } } + +//---------------------------- Others ------------------------------------------ + /** + * A job title. + */ + public static class JobTitleDetail extends StringDetail + { + public JobTitleDetail(String jobTitle) + { + super("Job Title", jobTitle); + } + } + + /** + * Represents a (personal) "about me" short description. + */ + public static class AboutMeDetail extends StringDetail + { + public AboutMeDetail(String description) + { + super("Description", description); + } + } }