/**
  * Sets the background of the button depending on its position in the button bar.
  *
  * @param button the button which background to set
  * @param gridX the position of the button in the grid
  * @param isLast indicates if this is the last button in the button bar
  */
 private void setButtonBg(SIPCommButton button, int gridX, boolean isLast) {
   if (!isLast) {
     if (gridX == 1)
       button.setBackgroundImage(ImageLoader.getImage(ImageLoader.CONTACT_LIST_BUTTON_BG_LEFT));
     else if (gridX > 1)
       button.setBackgroundImage(ImageLoader.getImage(ImageLoader.CONTACT_LIST_BUTTON_BG_MIDDLE));
   } else {
     if (gridX == 1) // We have only one button shown.
     button.setBackgroundImage(ImageLoader.getImage(ImageLoader.CONTACT_LIST_ONE_BUTTON_BG));
     else // We set the background of the last button in the toolbar
     button.setBackgroundImage(ImageLoader.getImage(ImageLoader.CONTACT_LIST_BUTTON_BG_RIGHT));
   }
 }
/**
 * The <tt>ContactListCellRenderer</tt> is the custom cell renderer used in the Jitsi's
 * <tt>ContactList</tt>. It extends JPanel instead of JLabel, which allows adding different buttons
 * and icons to the contact cell. The cell border and background are repainted.
 *
 * @author Yana Stamcheva
 * @author Lubomir Marinov
 * @author Adam Netocny
 */
public class ContactListTreeCellRenderer extends JPanel
    implements TreeCellRenderer, Icon, Skinnable {
  /** Serial version UID. */
  private static final long serialVersionUID = 0L;

  /** The default height of the avatar. */
  private static final int AVATAR_HEIGHT = 30;

  /** The default width of the avatar. */
  private static final int AVATAR_WIDTH = 30;

  /** The extended height of the avatar. */
  private static final int EXTENDED_AVATAR_HEIGHT = 45;

  /** The extended width of the avatar. */
  private static final int EXTENDED_AVATAR_WIDTH = 45;

  /** The default width of the button. */
  private static final int BUTTON_WIDTH = 26;

  /** The default height of the button. */
  private static final int BUTTON_HEIGHT = 27;

  /** Left border value. */
  private static final int LEFT_BORDER = 5;

  /** Left border value. */
  private static final int TOP_BORDER = 2;

  /** Bottom border value. */
  private static final int BOTTOM_BORDER = 2;

  /** Right border value. */
  private static final int RIGHT_BORDER = 2;

  /** The horizontal gap between columns in pixels; */
  private static final int H_GAP = 2;

  /** The vertical gap between rows in pixels; */
  private static final int V_GAP = 3;

  /** The separator image for the button toolbar. */
  private static final Image BUTTON_SEPARATOR_IMG =
      ImageLoader.getImage(ImageLoader.CONTACT_LIST_BUTTON_SEPARATOR);

  /** The icon used for opened groups. */
  private ImageIcon openedGroupIcon;

  /** The icon used for closed groups. */
  private ImageIcon closedGroupIcon;

  /** The foreground color for groups. */
  private Color groupForegroundColor;

  /** The foreground color for contacts. */
  private Color contactForegroundColor;

  /** The component showing the name of the contact or group. */
  private final JLabel nameLabel = new JLabel();

  /** The status message label. */
  private final JLabel displayDetailsLabel = new JLabel();

  /** The call button. */
  private final SIPCommButton callButton = new SIPCommButton();

  /** The call video button. */
  private final SIPCommButton callVideoButton = new SIPCommButton();

  /** The desktop sharing button. */
  private final SIPCommButton desktopSharingButton = new SIPCommButton();

  /** The chat button. */
  private final SIPCommButton chatButton = new SIPCommButton();

  /** The add contact button. */
  private final SIPCommButton addContactButton = new SIPCommButton();

  /** The constraints used to align components in the <tt>centerPanel</tt>. */
  private final GridBagConstraints constraints = new GridBagConstraints();

  /** The component showing the avatar or the contact count in the case of groups. */
  protected final JLabel rightLabel = new JLabel();

  /** The message received image. */
  private Image msgReceivedImage;

  /** The label containing the status icon. */
  private final JLabel statusLabel = new JLabel();

  /** The icon showing the contact status. */
  protected Icon statusIcon = new ImageIcon();

  /** Indicates if the current list cell is selected. */
  protected boolean isSelected = false;

  /** The index of the current cell. */
  protected int row = 0;

  /** Indicates if the current cell contains a leaf or a group. */
  protected TreeNode treeNode = null;

  /** The parent tree. */
  private TreeContactList treeContactList;

  /** A list of the custom action buttons for contacts UIContacts. */
  private List<JButton> customActionButtons;

  /** A list of the custom action buttons for groups. */
  private List<JButton> customActionButtonsUIGroup;

  /** The last added button. */
  private SIPCommButton lastAddedButton;

  /** Initializes the panel containing the node. */
  public ContactListTreeCellRenderer() {
    super(new GridBagLayout());

    loadSkin();

    this.setOpaque(true);
    this.nameLabel.setOpaque(false);

    this.displayDetailsLabel.setFont(getFont().deriveFont(9f));
    this.displayDetailsLabel.setForeground(Color.GRAY);

    this.rightLabel.setHorizontalAlignment(JLabel.RIGHT);

    // !! IMPORTANT: General insets used for all components if not
    // overwritten!
    constraints.insets = new Insets(0, 0, 0, H_GAP);

    constraints.anchor = GridBagConstraints.WEST;
    constraints.fill = GridBagConstraints.NONE;
    constraints.gridx = 0;
    constraints.gridy = 0;
    constraints.gridheight = 1;
    constraints.weightx = 0f;
    constraints.weighty = 1f;
    this.add(statusLabel, constraints);

    addLabels(1);

    callButton.addActionListener(
        new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            if (treeNode != null && treeNode instanceof ContactNode) {
              call(treeNode, callButton, false, false);
            }
          }
        });

    callVideoButton.addActionListener(
        new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            if (treeNode != null && treeNode instanceof ContactNode) {
              call(treeNode, callVideoButton, true, false);
            }
          }
        });

    desktopSharingButton.addActionListener(
        new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            if (treeNode != null && treeNode instanceof ContactNode) {
              call(treeNode, desktopSharingButton, true, true);
            }
          }
        });

    chatButton.addActionListener(
        new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            if (treeNode != null && treeNode instanceof ContactNode) {
              UIContact contactDescriptor = ((ContactNode) treeNode).getContactDescriptor();

              if (contactDescriptor.getDescriptor() instanceof MetaContact) {
                GuiActivator.getUIService()
                    .getChatWindowManager()
                    .startChat((MetaContact) contactDescriptor.getDescriptor());
              }
            }
          }
        });

    addContactButton.addActionListener(
        new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            if (treeNode != null && treeNode instanceof ContactNode) {
              UIContact contactDescriptor = ((ContactNode) treeNode).getContactDescriptor();

              // The add contact function has only sense for external
              // source contacts.
              if (contactDescriptor instanceof SourceUIContact) {
                addContact((SourceUIContact) contactDescriptor);
              }
            }
          }
        });

    initButtonToolTips();
    this.setToolTipText("");
  }

  /**
   * Returns this panel that has been configured to display the meta contact and meta contact group
   * cells.
   *
   * @param tree the source tree
   * @param value the tree node
   * @param selected indicates if the node is selected
   * @param expanded indicates if the node is expanded
   * @param leaf indicates if the node is a leaf
   * @param row indicates the row number of the node
   * @param hasFocus indicates if the node has the focus
   * @return this panel
   */
  public Component getTreeCellRendererComponent(
      JTree tree,
      Object value,
      boolean selected,
      boolean expanded,
      boolean leaf,
      int row,
      boolean hasFocus) {
    this.treeContactList = (TreeContactList) tree;
    this.row = row;
    this.isSelected = selected;
    this.treeNode = (TreeNode) value;

    this.rightLabel.setIcon(null);

    DefaultTreeContactList contactList = (DefaultTreeContactList) tree;

    setBorder();
    addLabels(1);

    // Set background color.
    if (contactList instanceof TreeContactList) {
      ContactListFilter filter = ((TreeContactList) contactList).getCurrentFilter();

      if (filter != null
          && filter.equals(TreeContactList.historyFilter)
          && value instanceof ContactNode
          && row % 2 == 0) {
        setBackground(Constants.CALL_HISTORY_EVEN_ROW_COLOR);
      } else {
        setBackground(Color.WHITE);
      }
    }

    // Make appropriate adjustments for contact nodes and group nodes.
    if (value instanceof ContactNode) {
      UIContactImpl contact = ((ContactNode) value).getContactDescriptor();

      String displayName = contact.getDisplayName();

      if ((displayName == null || displayName.trim().length() < 1)
          && !(contact instanceof ShowMoreContact)) {
        displayName = GuiActivator.getResources().getI18NString("service.gui.UNKNOWN");
      }

      this.nameLabel.setText(displayName);

      if (statusIcon != null
          && contactList.isContactActive(contact)
          && statusIcon instanceof ImageIcon) ((ImageIcon) statusIcon).setImage(msgReceivedImage);
      else statusIcon = contact.getStatusIcon();

      this.statusLabel.setIcon(statusIcon);

      this.nameLabel.setFont(this.getFont().deriveFont(Font.PLAIN));

      if (contactForegroundColor != null) nameLabel.setForeground(contactForegroundColor);

      // Initializes status message components if the given meta contact
      // contains a status message.
      this.initDisplayDetails(contact.getDisplayDetails());

      if (this.treeContactList.isContactButtonsVisible()) this.initButtonsPanel(contact);

      int avatarWidth, avatarHeight;

      if (isSelected && treeContactList.isContactButtonsVisible()) {
        avatarWidth = EXTENDED_AVATAR_WIDTH;
        avatarHeight = EXTENDED_AVATAR_HEIGHT;
      } else {
        avatarWidth = AVATAR_WIDTH;
        avatarHeight = AVATAR_HEIGHT;
      }

      Icon avatar = contact.getAvatar(isSelected, avatarWidth, avatarHeight);

      if (avatar != null) {
        this.rightLabel.setIcon(avatar);
      }

      if (contact instanceof ShowMoreContact) {
        rightLabel.setFont(rightLabel.getFont().deriveFont(12f));
        rightLabel.setForeground(Color.GRAY);
        rightLabel.setText((String) contact.getDescriptor());
      } else {
        rightLabel.setFont(rightLabel.getFont().deriveFont(9f));
        rightLabel.setText("");
      }

      this.setToolTipText(contact.getDescriptor().toString());
    } else if (value instanceof GroupNode) {
      UIGroupImpl groupItem = ((GroupNode) value).getGroupDescriptor();

      this.nameLabel.setText(groupItem.getDisplayName());

      this.nameLabel.setFont(this.getFont().deriveFont(Font.BOLD));

      if (groupForegroundColor != null) this.nameLabel.setForeground(groupForegroundColor);

      this.remove(displayDetailsLabel);
      this.remove(callButton);
      this.remove(callVideoButton);
      this.remove(desktopSharingButton);
      this.remove(chatButton);
      this.remove(addContactButton);

      clearCustomActionButtons();

      statusIcon = expanded ? openedGroupIcon : closedGroupIcon;
      this.statusLabel.setIcon(expanded ? openedGroupIcon : closedGroupIcon);

      // We have no photo icon for groups.
      this.rightLabel.setIcon(null);
      this.rightLabel.setText("");

      if (groupItem.countChildContacts() >= 0) {
        rightLabel.setFont(rightLabel.getFont().deriveFont(9f));
        this.rightLabel.setForeground(Color.BLACK);
        this.rightLabel.setText(
            groupItem.countOnlineChildContacts() + "/" + groupItem.countChildContacts());
      }

      this.initDisplayDetails(groupItem.getDisplayDetails());
      this.initButtonsPanel(groupItem);
      this.setToolTipText(groupItem.getDescriptor().toString());
    }

    return this;
  }

  /**
   * Paints a customized background.
   *
   * @param g the <tt>Graphics</tt> object through which we paint
   */
  @Override
  protected void paintComponent(Graphics g) {
    super.paintComponent(g);

    g = g.create();

    if (!(treeNode instanceof GroupNode) && !isSelected) return;

    AntialiasingManager.activateAntialiasing(g);

    Graphics2D g2 = (Graphics2D) g;

    try {
      internalPaintComponent(g2);
    } finally {
      g.dispose();
    }
  }

  /**
   * Paint a background for all groups and a round blue border and background when a cell is
   * selected.
   *
   * @param g2 the <tt>Graphics2D</tt> object through which we paint
   */
  private void internalPaintComponent(Graphics2D g2) {
    Color borderColor = Color.GRAY;

    if (isSelected) {
      g2.setPaint(
          new GradientPaint(
              0, 0, Constants.SELECTED_COLOR, 0, getHeight(), Constants.SELECTED_GRADIENT_COLOR));

      borderColor = Constants.SELECTED_COLOR;
    } else if (treeNode instanceof GroupNode) {
      g2.setPaint(
          new GradientPaint(
              0,
              0,
              Constants.CONTACT_LIST_GROUP_BG_GRADIENT_COLOR,
              0,
              getHeight(),
              Constants.CONTACT_LIST_GROUP_BG_COLOR));

      borderColor = Constants.CONTACT_LIST_GROUP_BG_COLOR;
    }

    g2.fillRect(0, 0, getWidth(), getHeight());
    g2.setColor(borderColor);
    g2.drawLine(0, 0, getWidth(), 0);
    g2.drawLine(0, getHeight() - 1, getWidth(), getHeight() - 1);
  }

  /**
   * Returns the height of this icon. Used for the drag&drop component.
   *
   * @return the height of this icon
   */
  public int getIconHeight() {
    return getPreferredSize().height + 10;
  }

  /**
   * Returns the width of this icon. Used for the drag&drop component.
   *
   * @return the widht of this icon
   */
  public int getIconWidth() {
    return treeContactList.getWidth() + 10;
  }

  /**
   * Returns the preferred size of this component.
   *
   * @return the preferred size of this component
   */
  public Dimension getPreferredSize() {
    Dimension preferredSize = new Dimension();
    int preferredHeight;

    if (treeNode instanceof ContactNode) {
      UIContact contact = ((ContactNode) treeNode).getContactDescriptor();

      preferredHeight = contact.getPreferredHeight();

      if (preferredHeight > 0) preferredSize.height = preferredHeight;
      else if (contact instanceof ShowMoreContact) preferredSize.height = 20;
      else if (isSelected && treeContactList.isContactButtonsVisible()) preferredSize.height = 70;
      else preferredSize.height = 35;
    } else if (treeNode instanceof GroupNode) {
      UIGroup group = ((GroupNode) treeNode).getGroupDescriptor();

      preferredHeight = group.getPreferredHeight();

      if (isSelected && customActionButtonsUIGroup != null && !customActionButtonsUIGroup.isEmpty())
        preferredSize.height = 70;
      else if (preferredHeight > 0) preferredSize.height = preferredHeight;
      else preferredSize.height = 20;
    }

    return preferredSize;
  }

  /**
   * Adds contact entry labels.
   *
   * @param nameLabelGridWidth the grid width of the contact entry name label
   */
  private void addLabels(int nameLabelGridWidth) {
    remove(nameLabel);
    remove(rightLabel);
    remove(displayDetailsLabel);

    if (treeNode != null && !(treeNode instanceof GroupNode))
      constraints.insets = new Insets(0, 0, V_GAP, H_GAP);
    else constraints.insets = new Insets(0, 0, 0, H_GAP);

    constraints.anchor = GridBagConstraints.WEST;
    constraints.fill = GridBagConstraints.NONE;
    constraints.gridx = 1;
    constraints.gridy = 0;
    constraints.weightx = 1f;
    constraints.weighty = 0f;
    constraints.gridheight = 1;
    constraints.gridwidth = nameLabelGridWidth;
    this.add(nameLabel, constraints);

    constraints.anchor = GridBagConstraints.NORTHEAST;
    constraints.fill = GridBagConstraints.VERTICAL;
    constraints.gridx = nameLabelGridWidth + 1;
    constraints.gridy = 0;
    constraints.gridheight = 3;
    constraints.weightx = 0f;
    constraints.weighty = 1f;
    this.add(rightLabel, constraints);

    if (treeNode != null && treeNode instanceof ContactNode) {
      constraints.anchor = GridBagConstraints.WEST;
      constraints.fill = GridBagConstraints.NONE;
      constraints.gridx = 1;
      constraints.gridy = 1;
      constraints.weightx = 1f;
      constraints.weighty = 0f;
      constraints.gridwidth = nameLabelGridWidth;
      constraints.gridheight = 1;

      this.add(displayDetailsLabel, constraints);
    } else if (treeNode != null && treeNode instanceof GroupNode) {
      constraints.anchor = GridBagConstraints.WEST;
      constraints.fill = GridBagConstraints.NONE;
      constraints.gridx = 1;
      constraints.gridy = 1;
      constraints.weightx = 1f;
      constraints.weighty = 0f;
      constraints.gridwidth = nameLabelGridWidth;
      constraints.gridheight = 1;

      this.add(displayDetailsLabel, constraints);
    }
  }

  /**
   * Initializes the display details component for the given <tt>UIContact</tt>.
   *
   * @param displayDetails the display details to show
   */
  private void initDisplayDetails(String displayDetails) {
    remove(displayDetailsLabel);
    displayDetailsLabel.setText("");

    if (displayDetails != null && displayDetails.length() > 0) {
      // Replace all occurrences of new line with slash.
      displayDetails = Html2Text.extractText(displayDetails);
      displayDetails = displayDetails.replaceAll("\n|<br>|<br/>", " / ");

      displayDetailsLabel.setText(displayDetails);
    }

    constraints.anchor = GridBagConstraints.WEST;
    constraints.fill = GridBagConstraints.NONE;
    constraints.gridx = 1;
    constraints.gridy = 1;
    constraints.weightx = 1f;
    constraints.weighty = 0f;
    constraints.gridwidth = 1;
    constraints.gridheight = 1;

    this.add(displayDetailsLabel, constraints);
  }

  /**
   * Initializes buttons panel.
   *
   * @param uiContact the <tt>UIContact</tt> for which we initialize the button panel
   */
  private void initButtonsPanel(UIContact uiContact) {
    this.remove(chatButton);
    this.remove(callButton);
    this.remove(callVideoButton);
    this.remove(desktopSharingButton);
    this.remove(addContactButton);

    clearCustomActionButtons();

    if (!isSelected) return;

    UIContactDetail imContact = null;
    // For now we support instance messaging only for contacts in our
    // contact list until it's implemented for external source contacts.
    if (uiContact.getDescriptor() instanceof MetaContact)
      imContact = uiContact.getDefaultContactDetail(OperationSetBasicInstantMessaging.class);

    int x = (statusIcon == null ? 0 : statusIcon.getIconWidth()) + LEFT_BORDER + H_GAP;

    // Re-initialize the x grid.
    constraints.gridx = 0;
    int gridX = 0;

    if (imContact != null) {
      x += addButton(chatButton, ++gridX, x, false);
    }

    UIContactDetail telephonyContact =
        uiContact.getDefaultContactDetail(OperationSetBasicTelephony.class);

    // Check if contact has additional phone numbers, if yes show the
    // call button
    ContactPhoneUtil contactPhoneUtil = null;

    // check for phone stored in contact info only
    // if telephony contact is missing
    if (uiContact.getDescriptor() != null
        && uiContact.getDescriptor() instanceof MetaContact
        && telephonyContact == null) {
      contactPhoneUtil = ContactPhoneUtil.getPhoneUtil((MetaContact) uiContact.getDescriptor());

      MetaContact metaContact = (MetaContact) uiContact.getDescriptor();
      Iterator<Contact> contacts = metaContact.getContacts();

      while (contacts.hasNext()) // && !hasPhone)
      {
        Contact contact = contacts.next();

        if (!contact.getProtocolProvider().isRegistered()) continue;

        contactPhoneUtil.addDetailsResponseListener(
            contact, new DetailsListener(treeNode, callButton, uiContact));
      }
    }

    // for SourceContact in history that do not support telephony, we
    // show the button but disabled
    List<ProtocolProviderService> providers =
        AccountUtils.getOpSetRegisteredProviders(OperationSetBasicTelephony.class, null, null);

    if ((telephonyContact != null && telephonyContact.getAddress() != null)
        || (contactPhoneUtil != null && contactPhoneUtil.isCallEnabled() && providers.size() > 0)) {
      x += addButton(callButton, ++gridX, x, false);
    }

    UIContactDetail videoContact =
        uiContact.getDefaultContactDetail(OperationSetVideoTelephony.class);

    if (videoContact != null
        || (contactPhoneUtil != null && contactPhoneUtil.isVideoCallEnabled())) {
      x += addButton(callVideoButton, ++gridX, x, false);
    }

    UIContactDetail desktopContact =
        uiContact.getDefaultContactDetail(OperationSetDesktopSharingServer.class);

    if (desktopContact != null
        || (contactPhoneUtil != null && contactPhoneUtil.isDesktopSharingEnabled())) {
      x += addButton(desktopSharingButton, ++gridX, x, false);
    }

    // enable add contact button if contact source has indicated
    // that this is possible
    if (uiContact.getDescriptor() instanceof SourceContact
        && uiContact.getDefaultContactDetail(OperationSetPersistentPresence.class) != null
        && AccountUtils.getOpSetRegisteredProviders(
                    OperationSetPersistentPresence.class, null, null)
                .size()
            > 0
        && !ConfigurationUtils.isAddContactDisabled()) {
      x += addButton(addContactButton, ++gridX, x, false);
    }

    // The list of the contact actions
    // we will create a button for every action
    Collection<SIPCommButton> contactActions = uiContact.getContactCustomActionButtons();

    int lastGridX = gridX;
    if (contactActions != null && contactActions.size() > 0) {
      lastGridX = initContactActionButtons(contactActions, gridX, x);
    } else {
      addLabels(gridX);
    }

    if (lastAddedButton != null) setButtonBg(lastAddedButton, lastGridX, true);

    this.setBounds(0, 0, treeContactList.getWidth(), getPreferredSize().height);
  }

  /**
   * Initializes buttons panel.
   *
   * @param uiGroup the <tt>UIGroup</tt> for which we initialize the button panel
   */
  private void initButtonsPanel(UIGroup uiGroup) {
    if (!isSelected) return;

    int x = (statusIcon == null ? 0 : statusIcon.getIconWidth()) + LEFT_BORDER + H_GAP;
    int gridX = 0;

    // The list of the actions
    // we will create a button for every action
    Collection<SIPCommButton> contactActions = uiGroup.getCustomActionButtons();

    int lastGridX = gridX;
    if (contactActions != null && contactActions.size() > 0) {
      lastGridX = initGroupActionButtons(contactActions, gridX, x);
    } else {
      addLabels(gridX);
    }

    if (lastAddedButton != null) setButtonBg(lastAddedButton, lastGridX, true);

    this.setBounds(0, 0, treeContactList.getWidth(), getPreferredSize().height);
  }

  /** Clears the custom action buttons. */
  private void clearCustomActionButtons() {
    if (customActionButtons != null && customActionButtons.size() > 0) {
      Iterator<JButton> buttonsIter = customActionButtons.iterator();
      while (buttonsIter.hasNext()) {
        remove(buttonsIter.next());
      }
      customActionButtons.clear();
    }

    if (customActionButtonsUIGroup != null && customActionButtonsUIGroup.size() > 0) {
      Iterator<JButton> buttonsIter = customActionButtonsUIGroup.iterator();
      while (buttonsIter.hasNext()) {
        remove(buttonsIter.next());
      }
      customActionButtonsUIGroup.clear();
    }
  }

  /**
   * Initializes custom contact action buttons.
   *
   * @param contactActionButtons the list of buttons to initialize
   * @param gridX the X grid of the first button
   * @param xBounds the x bounds of the first button
   * @return the new grid X coordinate after adding all the buttons
   */
  private int initGroupActionButtons(
      Collection<SIPCommButton> contactActionButtons, int gridX, int xBounds) {
    // Reinit the labels to take the whole horizontal space.
    addLabels(gridX + contactActionButtons.size());

    Iterator<SIPCommButton> actionsIter = contactActionButtons.iterator();
    while (actionsIter.hasNext()) {
      final SIPCommButton actionButton = actionsIter.next();

      // We need to explicitly remove the buttons from the tooltip manager,
      // because we're going to manager the tooltip ourselves in the
      // DefaultTreeContactList class. We need to do this in order to have
      // a different tooltip for every button and for non button area.
      ToolTipManager.sharedInstance().unregisterComponent(actionButton);

      if (customActionButtonsUIGroup == null)
        customActionButtonsUIGroup = new LinkedList<JButton>();

      customActionButtonsUIGroup.add(actionButton);

      xBounds += addButton(actionButton, ++gridX, xBounds, false);
    }

    return gridX;
  }

  /**
   * Initializes custom contact action buttons.
   *
   * @param contactActionButtons the list of buttons to initialize
   * @param gridX the X grid of the first button
   * @param xBounds the x bounds of the first button
   * @return the new grid X coordiante after adding all the buttons
   */
  private int initContactActionButtons(
      Collection<SIPCommButton> contactActionButtons, int gridX, int xBounds) {
    // Reinit the labels to take the whole horizontal space.
    addLabels(gridX + contactActionButtons.size());

    Iterator<SIPCommButton> actionsIter = contactActionButtons.iterator();
    while (actionsIter.hasNext()) {
      final SIPCommButton actionButton = actionsIter.next();

      // We need to explicitly remove the buttons from the tooltip manager,
      // because we're going to manager the tooltip ourselves in the
      // DefaultTreeContactList class. We need to do this in order to have
      // a different tooltip for every button and for non button area.
      ToolTipManager.sharedInstance().unregisterComponent(actionButton);

      if (customActionButtons == null) customActionButtons = new LinkedList<JButton>();

      customActionButtons.add(actionButton);

      xBounds += addButton(actionButton, ++gridX, xBounds, false);
    }

    return gridX;
  }

  /**
   * Draw the icon at the specified location. Paints this component as an icon.
   *
   * @param c the component which can be used as observer
   * @param g the <tt>Graphics</tt> object used for painting
   * @param x the position on the X coordinate
   * @param y the position on the Y coordinate
   */
  public void paintIcon(Component c, Graphics g, int x, int y) {
    g = g.create();
    try {
      Graphics2D g2 = (Graphics2D) g;
      AntialiasingManager.activateAntialiasing(g2);

      g2.setColor(Color.WHITE);
      g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.8f));
      g2.fillRoundRect(x, y, getIconWidth() - 1, getIconHeight() - 1, 10, 10);
      g2.setColor(Color.DARK_GRAY);
      g2.drawRoundRect(x, y, getIconWidth() - 1, getIconHeight() - 1, 10, 10);

      // Indent component content from the border.
      g2.translate(x + 5, y + 5);

      super.paint(g2);

      g2.translate(x, y);
    } finally {
      g.dispose();
    }
  }

  /**
   * Returns the call button contained in the current cell.
   *
   * @return the call button contained in the current cell
   */
  public JButton getChatButton() {
    return chatButton;
  }

  /**
   * Returns the call button contained in the current cell.
   *
   * @return the call button contained in the current cell
   */
  public JButton getCallButton() {
    return callButton;
  }

  /**
   * Returns the call video button contained in the current cell.
   *
   * @return the call video button contained in the current cell
   */
  public JButton getCallVideoButton() {
    return callVideoButton;
  }

  /**
   * Returns the desktop sharing button contained in the current cell.
   *
   * @return the desktop sharing button contained in the current cell
   */
  public JButton getDesktopSharingButton() {
    return desktopSharingButton;
  }

  /**
   * Returns the add contact button contained in the current cell.
   *
   * @return the add contact button contained in the current cell
   */
  public JButton getAddContactButton() {
    return addContactButton;
  }

  /**
   * Calls the given treeNode.
   *
   * @param treeNode the <tt>TreeNode</tt> to call
   */
  private void call(TreeNode treeNode, JButton button, boolean isVideo, boolean isDesktopSharing) {
    if (!(treeNode instanceof ContactNode)) return;

    UIContact contactDescriptor = ((ContactNode) treeNode).getContactDescriptor();

    Point location = new Point(button.getX(), button.getY() + button.getHeight());

    SwingUtilities.convertPointToScreen(location, treeContactList);

    location.y = location.y + treeContactList.getPathBounds(treeContactList.getSelectionPath()).y;
    location.x += 8;
    location.y -= 8;

    CallManager.call(contactDescriptor, isVideo, isDesktopSharing, treeContactList, location);
  }

  /**
   * Shows the appropriate user interface that would allow the user to add the given
   * <tt>SourceUIContact</tt> to their contact list.
   *
   * @param contact the contact to add
   */
  private void addContact(SourceUIContact contact) {
    SourceContact sourceContact = (SourceContact) contact.getDescriptor();

    List<ContactDetail> details =
        sourceContact.getContactDetails(OperationSetPersistentPresence.class);
    int detailsCount = details.size();

    if (detailsCount > 1) {
      JMenuItem addContactMenu =
          TreeContactList.createAddContactMenu((SourceContact) contact.getDescriptor());

      JPopupMenu popupMenu = ((JMenu) addContactMenu).getPopupMenu();

      // Add a title label.
      JLabel infoLabel = new JLabel();
      infoLabel.setText(
          "<html><b>"
              + GuiActivator.getResources().getI18NString("service.gui.ADD_CONTACT")
              + "</b></html>");

      popupMenu.insert(infoLabel, 0);
      popupMenu.insert(new Separator(), 1);

      popupMenu.setFocusable(true);
      popupMenu.setInvoker(treeContactList);

      Point location =
          new Point(
              addContactButton.getX(), addContactButton.getY() + addContactButton.getHeight());

      SwingUtilities.convertPointToScreen(location, treeContactList);

      location.y = location.y + treeContactList.getPathBounds(treeContactList.getSelectionPath()).y;

      popupMenu.setLocation(location.x + 8, location.y - 8);
      popupMenu.setVisible(true);
    } else if (details.size() == 1) {
      TreeContactList.showAddContactDialog(details.get(0), sourceContact.getDisplayName());
    }
  }

  /**
   * Returns the drag icon used to represent a cell in all drag operations.
   *
   * @param tree the parent tree object
   * @param dragObject the dragged object
   * @param index the index of the dragged object in the tree
   * @return the drag icon
   */
  public Icon getDragIcon(JTree tree, Object dragObject, int index) {
    ContactListTreeCellRenderer dragC =
        (ContactListTreeCellRenderer)
            getTreeCellRendererComponent(
                tree,
                dragObject,
                false, // is selected
                false, // is expanded
                true, // is leaf
                index,
                true // has focus
                );

    // We should explicitly set the bounds of all components in order that
    // they're correctly painted by paintIcon afterwards. This fixes empty
    // drag component in contact list!
    dragC.setBounds(0, 0, dragC.getIconWidth(), dragC.getIconHeight());

    Icon rightLabelIcon = rightLabel.getIcon();
    int imageHeight = 0;
    int imageWidth = 0;
    if (rightLabelIcon != null) {
      imageWidth = rightLabelIcon.getIconWidth();
      imageHeight = rightLabelIcon.getIconHeight();
      dragC.rightLabel.setBounds(tree.getWidth() - imageWidth, 0, imageWidth, imageHeight);
    }

    dragC.statusLabel.setBounds(0, 0, statusLabel.getWidth(), statusLabel.getHeight());

    dragC.nameLabel.setBounds(
        statusLabel.getWidth(), 0, tree.getWidth() - imageWidth - 5, nameLabel.getHeight());

    dragC.displayDetailsLabel.setBounds(
        displayDetailsLabel.getX(),
        nameLabel.getHeight(),
        displayDetailsLabel.getWidth(),
        displayDetailsLabel.getHeight());

    return dragC;
  }

  /** Resets the rollover state of all rollover components in the current cell. */
  public void resetRolloverState() {
    chatButton.getModel().setRollover(false);
    callButton.getModel().setRollover(false);
    callVideoButton.getModel().setRollover(false);
    desktopSharingButton.getModel().setRollover(false);
    addContactButton.getModel().setRollover(false);

    if (customActionButtons != null) {
      Iterator<JButton> buttonsIter = customActionButtons.iterator();
      while (buttonsIter.hasNext()) {
        JButton button = buttonsIter.next();
        button.getModel().setRollover(false);
      }
    }

    if (customActionButtonsUIGroup != null) {
      Iterator<JButton> buttonsIter = customActionButtonsUIGroup.iterator();
      while (buttonsIter.hasNext()) {
        JButton button = buttonsIter.next();
        button.getModel().setRollover(false);
      }
    }
  }

  /**
   * Resets the rollover state of all rollover components in the current cell except the component
   * given as a parameter.
   *
   * @param excludeComponent the component to exclude from the reset
   */
  public void resetRolloverState(Component excludeComponent) {
    if (!chatButton.equals(excludeComponent)) chatButton.getModel().setRollover(false);

    if (!callButton.equals(excludeComponent)) callButton.getModel().setRollover(false);

    if (!callVideoButton.equals(excludeComponent)) callVideoButton.getModel().setRollover(false);

    if (!desktopSharingButton.equals(excludeComponent))
      desktopSharingButton.getModel().setRollover(false);

    if (!addContactButton.equals(excludeComponent)) addContactButton.getModel().setRollover(false);

    if (customActionButtons != null) {
      Iterator<JButton> buttonsIter = customActionButtons.iterator();
      while (buttonsIter.hasNext()) {
        JButton button = buttonsIter.next();

        if (!button.equals(excludeComponent)) button.getModel().setRollover(false);
      }
    }

    if (customActionButtonsUIGroup != null) {
      Iterator<JButton> buttonsIter = customActionButtonsUIGroup.iterator();
      while (buttonsIter.hasNext()) {
        JButton button = buttonsIter.next();

        if (!button.equals(excludeComponent)) button.getModel().setRollover(false);
      }
    }
  }

  /** Loads all images and colors. */
  public void loadSkin() {
    openedGroupIcon = new ImageIcon(ImageLoader.getImage(ImageLoader.OPENED_GROUP_ICON));

    closedGroupIcon = new ImageIcon(ImageLoader.getImage(ImageLoader.CLOSED_GROUP_ICON));

    callButton.setIconImage(ImageLoader.getImage(ImageLoader.CALL_BUTTON_SMALL));
    callButton.setRolloverIcon(ImageLoader.getImage(ImageLoader.CALL_BUTTON_SMALL_ROLLOVER));
    callButton.setPressedIcon(ImageLoader.getImage(ImageLoader.CALL_BUTTON_SMALL_PRESSED));

    chatButton.setIconImage(ImageLoader.getImage(ImageLoader.CHAT_BUTTON_SMALL));
    chatButton.setRolloverIcon(ImageLoader.getImage(ImageLoader.CHAT_BUTTON_SMALL_ROLLOVER));
    chatButton.setPressedIcon(ImageLoader.getImage(ImageLoader.CHAT_BUTTON_SMALL_PRESSED));

    msgReceivedImage = ImageLoader.getImage(ImageLoader.MESSAGE_RECEIVED_ICON);

    int groupForegroundProperty =
        GuiActivator.getResources().getColor("service.gui.CONTACT_LIST_GROUP_FOREGROUND");

    if (groupForegroundProperty > -1) groupForegroundColor = new Color(groupForegroundProperty);

    int contactForegroundProperty =
        GuiActivator.getResources().getColor("service.gui.CONTACT_LIST_CONTACT_FOREGROUND");

    if (contactForegroundProperty > -1)
      contactForegroundColor = new Color(contactForegroundProperty);

    callVideoButton.setIconImage(ImageLoader.getImage(ImageLoader.CALL_VIDEO_BUTTON_SMALL));
    callVideoButton.setRolloverIcon(
        ImageLoader.getImage(ImageLoader.CALL_VIDEO_BUTTON_SMALL_ROLLOVER));
    callVideoButton.setPressedIcon(
        ImageLoader.getImage(ImageLoader.CALL_VIDEO_BUTTON_SMALL_PRESSED));

    desktopSharingButton.setIconImage(ImageLoader.getImage(ImageLoader.DESKTOP_BUTTON_SMALL));
    desktopSharingButton.setRolloverIcon(
        ImageLoader.getImage(ImageLoader.DESKTOP_BUTTON_SMALL_ROLLOVER));
    desktopSharingButton.setPressedIcon(
        ImageLoader.getImage(ImageLoader.DESKTOP_BUTTON_SMALL_PRESSED));

    addContactButton.setIconImage(ImageLoader.getImage(ImageLoader.ADD_CONTACT_BUTTON_SMALL));
    addContactButton.setRolloverIcon(
        ImageLoader.getImage(ImageLoader.ADD_CONTACT_BUTTON_SMALL_ROLLOVER));
    addContactButton.setPressedIcon(
        ImageLoader.getImage(ImageLoader.ADD_CONTACT_BUTTON_SMALL_PRESSED));
  }

  /**
   * Listens for contact details if not cached, we will receive when they are retrieved to update
   * current call button state, if meanwhile user hasn't changed the current contact.
   */
  private class DetailsListener
      implements OperationSetServerStoredContactInfo.DetailsResponseListener {
    /** The source this listener is created for, if current tree node changes ignore any event. */
    private Object source;

    /** The button to change. */
    private JButton callButton;

    /** The ui contact to update after changes. */
    private UIContact uiContact;

    /**
     * Create listener.
     *
     * @param source the contact this listener is for, if different than current ignore.
     * @param callButton
     * @param uiContact the contact to refresh
     */
    DetailsListener(Object source, JButton callButton, UIContact uiContact) {
      this.source = source;
      this.callButton = callButton;
      this.uiContact = uiContact;
    }

    /**
     * Details have been retrieved.
     *
     * @param details the details retrieved if any.
     */
    public void detailsRetrieved(Iterator<GenericDetail> details) {
      // if treenode has changed ignore
      if (!source.equals(treeNode)) return;

      while (details.hasNext()) {
        GenericDetail d = details.next();

        if (d instanceof PhoneNumberDetail
            && !(d instanceof PagerDetail)
            && !(d instanceof FaxDetail)) {
          final PhoneNumberDetail pnd = (PhoneNumberDetail) d;
          if (pnd.getNumber() != null && pnd.getNumber().length() > 0) {
            SwingUtilities.invokeLater(
                new Runnable() {
                  public void run() {
                    callButton.setEnabled(true);

                    if (pnd instanceof VideoDetail) {
                      callVideoButton.setEnabled(true);
                      desktopSharingButton.setEnabled(true);
                    }

                    treeContactList.refreshContact(uiContact);
                  }
                });

            return;
          }
        }
      }
    }
  }

  private int addButton(SIPCommButton button, int gridX, int xBounds, boolean isLast) {
    lastAddedButton = button;

    constraints.insets = new Insets(0, 0, V_GAP, 0);
    constraints.anchor = GridBagConstraints.WEST;
    constraints.fill = GridBagConstraints.NONE;
    constraints.gridx = gridX;
    constraints.gridy = 2;
    constraints.gridwidth = 1;
    constraints.gridheight = 1;
    constraints.weightx = 0f;
    constraints.weighty = 0f;
    this.add(button, constraints);

    int yBounds =
        TOP_BORDER
            + BOTTOM_BORDER
            + 2 * V_GAP
            + ComponentUtils.getStringSize(nameLabel, nameLabel.getText()).height
            + ComponentUtils.getStringSize(displayDetailsLabel, displayDetailsLabel.getText())
                .height;

    button.setBounds(xBounds, yBounds, BUTTON_WIDTH, BUTTON_HEIGHT);

    button.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));

    setButtonBg(button, gridX, isLast);

    return button.getWidth();
  }

  /**
   * Sets the background of the button depending on its position in the button bar.
   *
   * @param button the button which background to set
   * @param gridX the position of the button in the grid
   * @param isLast indicates if this is the last button in the button bar
   */
  private void setButtonBg(SIPCommButton button, int gridX, boolean isLast) {
    if (!isLast) {
      if (gridX == 1)
        button.setBackgroundImage(ImageLoader.getImage(ImageLoader.CONTACT_LIST_BUTTON_BG_LEFT));
      else if (gridX > 1)
        button.setBackgroundImage(ImageLoader.getImage(ImageLoader.CONTACT_LIST_BUTTON_BG_MIDDLE));
    } else {
      if (gridX == 1) // We have only one button shown.
      button.setBackgroundImage(ImageLoader.getImage(ImageLoader.CONTACT_LIST_ONE_BUTTON_BG));
      else // We set the background of the last button in the toolbar
      button.setBackgroundImage(ImageLoader.getImage(ImageLoader.CONTACT_LIST_BUTTON_BG_RIGHT));
    }
  }

  /** Sets the correct border depending on the contained object. */
  private void setBorder() {
    /*
     * !!! When changing border values we should make sure that we
     * recalculate the X and Y coordinates of the buttons added in
     * initButtonsPanel and initContactActionButtons functions. If not
     * correctly calculated problems may occur when clicking buttons!
     */
    if (treeNode instanceof ContactNode
        && !(((ContactNode) treeNode).getContactDescriptor() instanceof ShowMoreContact)) {
      this.setBorder(
          BorderFactory.createEmptyBorder(TOP_BORDER, LEFT_BORDER, BOTTOM_BORDER, RIGHT_BORDER));
    } else // GroupNode || ShowMoreContact
    {
      this.setBorder(BorderFactory.createEmptyBorder(0, LEFT_BORDER, 0, RIGHT_BORDER));
    }
  }

  /** Inializes button tool tips. */
  private void initButtonToolTips() {
    callButton.setToolTipText(
        GuiActivator.getResources().getI18NString("service.gui.CALL_CONTACT"));
    callVideoButton.setToolTipText(
        GuiActivator.getResources().getI18NString("service.gui.VIDEO_CALL"));
    desktopSharingButton.setToolTipText(
        GuiActivator.getResources().getI18NString("service.gui.SHARE_DESKTOP"));
    chatButton.setToolTipText(
        GuiActivator.getResources().getI18NString("service.gui.SEND_MESSAGE"));
    addContactButton.setToolTipText(
        GuiActivator.getResources().getI18NString("service.gui.ADD_CONTACT"));

    // We need to explicitly remove the buttons from the tooltip manager,
    // because we're going to manager the tooltip ourselves in the
    // DefaultTreeContactList class. We need to do this in order to have
    // a different tooltip for every button and for non button area.
    ToolTipManager ttManager = ToolTipManager.sharedInstance();
    ttManager.unregisterComponent(callButton);
    ttManager.unregisterComponent(callVideoButton);
    ttManager.unregisterComponent(desktopSharingButton);
    ttManager.unregisterComponent(chatButton);
    ttManager.unregisterComponent(addContactButton);
  }
}
  /** Loads all images and colors. */
  public void loadSkin() {
    openedGroupIcon = new ImageIcon(ImageLoader.getImage(ImageLoader.OPENED_GROUP_ICON));

    closedGroupIcon = new ImageIcon(ImageLoader.getImage(ImageLoader.CLOSED_GROUP_ICON));

    callButton.setIconImage(ImageLoader.getImage(ImageLoader.CALL_BUTTON_SMALL));
    callButton.setRolloverIcon(ImageLoader.getImage(ImageLoader.CALL_BUTTON_SMALL_ROLLOVER));
    callButton.setPressedIcon(ImageLoader.getImage(ImageLoader.CALL_BUTTON_SMALL_PRESSED));

    chatButton.setIconImage(ImageLoader.getImage(ImageLoader.CHAT_BUTTON_SMALL));
    chatButton.setRolloverIcon(ImageLoader.getImage(ImageLoader.CHAT_BUTTON_SMALL_ROLLOVER));
    chatButton.setPressedIcon(ImageLoader.getImage(ImageLoader.CHAT_BUTTON_SMALL_PRESSED));

    msgReceivedImage = ImageLoader.getImage(ImageLoader.MESSAGE_RECEIVED_ICON);

    int groupForegroundProperty =
        GuiActivator.getResources().getColor("service.gui.CONTACT_LIST_GROUP_FOREGROUND");

    if (groupForegroundProperty > -1) groupForegroundColor = new Color(groupForegroundProperty);

    int contactForegroundProperty =
        GuiActivator.getResources().getColor("service.gui.CONTACT_LIST_CONTACT_FOREGROUND");

    if (contactForegroundProperty > -1)
      contactForegroundColor = new Color(contactForegroundProperty);

    callVideoButton.setIconImage(ImageLoader.getImage(ImageLoader.CALL_VIDEO_BUTTON_SMALL));
    callVideoButton.setRolloverIcon(
        ImageLoader.getImage(ImageLoader.CALL_VIDEO_BUTTON_SMALL_ROLLOVER));
    callVideoButton.setPressedIcon(
        ImageLoader.getImage(ImageLoader.CALL_VIDEO_BUTTON_SMALL_PRESSED));

    desktopSharingButton.setIconImage(ImageLoader.getImage(ImageLoader.DESKTOP_BUTTON_SMALL));
    desktopSharingButton.setRolloverIcon(
        ImageLoader.getImage(ImageLoader.DESKTOP_BUTTON_SMALL_ROLLOVER));
    desktopSharingButton.setPressedIcon(
        ImageLoader.getImage(ImageLoader.DESKTOP_BUTTON_SMALL_PRESSED));

    addContactButton.setIconImage(ImageLoader.getImage(ImageLoader.ADD_CONTACT_BUTTON_SMALL));
    addContactButton.setRolloverIcon(
        ImageLoader.getImage(ImageLoader.ADD_CONTACT_BUTTON_SMALL_ROLLOVER));
    addContactButton.setPressedIcon(
        ImageLoader.getImage(ImageLoader.ADD_CONTACT_BUTTON_SMALL_PRESSED));
  }