diff options
35 files changed, 3090 insertions, 17 deletions
@@ -1389,7 +1389,7 @@ javax.swing.event, javax.swing.border"/> manifest="${src}/net/java/sip/communicator/impl/protocol/msn/msn.provider.manifest.mf"> <zipfileset dir="${dest}/net/java/sip/communicator/impl/protocol/msn" prefix="net/java/sip/communicator/impl/protocol/msn"/> - <zipfileset src="${lib.noinst}/jml-1.0b2.jar" prefix=""/> + <zipfileset src="${lib.noinst}/jml-1.0b5.jar" prefix=""/> </jar> </target> @@ -1400,7 +1400,7 @@ javax.swing.event, javax.swing.border"/> manifest="${testsrc}/net/java/sip/communicator/slick/protocol/msn/msn.provider.slick.manifest.mf"> <zipfileset dir="${dest}/net/java/sip/communicator/slick/protocol/msn" prefix="net/java/sip/communicator/slick/protocol/msn"/> - <zipfileset src="${lib.noinst}/jml-1.0b2.jar" prefix=""/> + <zipfileset src="${lib.noinst}/jml-1.0b5.jar" prefix=""/> <zipfileset dir="${dest}/net/java/sip/communicator/slick/protocol/generic" prefix="net/java/sip/communicator/slick/protocol/generic"/> </jar> diff --git a/ide/eclipse/.classpath b/ide/eclipse/.classpath index 5b2eea0..d34efd1 100644 --- a/ide/eclipse/.classpath +++ b/ide/eclipse/.classpath @@ -23,7 +23,7 @@ <classpathentry kind="lib" path="lib/installer-exclude/jdom.jar"/> <classpathentry kind="lib" path="lib/installer-exclude/jfontchooser-1.0.5.jar"/> <classpathentry kind="lib" path="lib/installer-exclude/jmf.jar"/> - <classpathentry kind="lib" path="lib/installer-exclude/jml-1.0b2.jar"/> + <classpathentry kind="lib" path="lib/installer-exclude/jml-1.0b5.jar"/> <classpathentry kind="lib" path="lib/installer-exclude/jna.jar"/> <classpathentry kind="lib" path="lib/installer-exclude/joscar-client.jar"/> <classpathentry kind="lib" path="lib/installer-exclude/joscar-common.jar"/> diff --git a/ide/nbproject/project.xml b/ide/nbproject/project.xml index e88d18a..1b87782 100644 --- a/ide/nbproject/project.xml +++ b/ide/nbproject/project.xml @@ -123,7 +123,7 @@ <compilation-unit> <package-root>src</package-root> <classpath - mode="compile">lib/felix.jar:lib/jdic-all.jar:lib/bundle/junit.jar:lib/bundle/log4j.jar:lib/bundle/commons-logging.jar:lib/installer-exclude/concurrent.jar:lib/installer-exclude/dict4j.jar:lib/installer-exclude/dnsjava-2.0.3.jar:lib/installer-exclude/jain-sip-api.jar:lib/installer-exclude/jain-sip-ri.jar:lib/installer-exclude/jain-sdp.jar:lib/installer-exclude/jcalendar-1.3.2.jar:lib/installer-exclude/jdic_misc.jar:lib/installer-exclude/jdom.jar:lib/installer-exclude/jmf.jar:lib/installer-exclude/jml-1.0b2.jar:lib/installer-exclude/joscar-client.jar:lib/installer-exclude/joscar-common.jar:lib/installer-exclude/joscar-protocol.jar:lib/installer-exclude/jsocks-klea.jar:lib/installer-exclude/jspeex.jar:lib/installer-exclude/junit.jar:lib/installer-exclude/log4j-1.2.8.jar:lib/installer-exclude/nist-sdp-1.0.jar:lib/installer-exclude/rome-0.9.jar:lib/installer-exclude/smack.jar:lib/installer-exclude/smackx.jar:lib/installer-exclude/Stun4J.jar:lib/installer-exclude/ymsg_network_v0_67.jar:lib/installer-exclude/fmj.jar:lib/installer-exclude/jna.jar:lib/installer-exclude/lti-civil-no_s_w_t.jar:lib/installer-exclude/swing-worker-1.2.jar:lib/os-specific/linux/installer-exclude/jmf.jar:lib/os-specific/linux/jdic_stub.jar:lib/os-specific/mac/OrangeExtensions.jar:lib/os-specific/mac/growl4j.jar:lib/os-specific/mac/jdic_stub.jar:lib/os-specific/mac/installer-exclude/jmf.jar:lib/os-specific/mac/installer-exclude/dock.jar:lib/os-specific/windows/jdic_stub.jar:lib/os-specific/windows/installer-exclude/jmf.jar:lib/os-specific/windows/installer-exclude/sound.jar:lib/installer-exclude/aclibico-2.1.jar:lib/installer-exclude/jdic_misc.jar:lib/installer-exclude/pircbot.jar:lib/os-specific/solaris/jdic_stub.jar:lib/os-specific/solaris/installer-exclude/jmf.jar:lib/installer-exclude/jsch-0.1.36.jar:lib/installer-exclude/apache-ant-1.7.0.jar:lib/installer-exclude/izpack-shortcut-link.jar:lib/installer-exclude/jfontchooser-1.0.5.jar:lib/installer-exclude/laf-widget.jar:lib/installer-exclude/transparency.jar:lib/installer-exclude/zrtp4j-light.jar:lib/installer-exclude/lcrypto-jdk16-143.jar:lib/installer-exclude/otr4j.jar:lib/installer-exclude/profiler4j-1.0-beta3-SC.jar:lib/installer-exclude/httpcore-4.0.1.jar:lib/installer-exclude/httpclient-4.0.1.jar:lib/installer-exclude/json-20090723.jar</classpath> + mode="compile">lib/felix.jar:lib/jdic-all.jar:lib/bundle/junit.jar:lib/bundle/log4j.jar:lib/bundle/commons-logging.jar:lib/installer-exclude/concurrent.jar:lib/installer-exclude/dict4j.jar:lib/installer-exclude/dnsjava-2.0.3.jar:lib/installer-exclude/jain-sip-api.jar:lib/installer-exclude/jain-sip-ri.jar:lib/installer-exclude/jain-sdp.jar:lib/installer-exclude/jcalendar-1.3.2.jar:lib/installer-exclude/jdic_misc.jar:lib/installer-exclude/jdom.jar:lib/installer-exclude/jmf.jar:lib/installer-exclude/jml-1.0b5.jar:lib/installer-exclude/joscar-client.jar:lib/installer-exclude/joscar-common.jar:lib/installer-exclude/joscar-protocol.jar:lib/installer-exclude/jsocks-klea.jar:lib/installer-exclude/jspeex.jar:lib/installer-exclude/junit.jar:lib/installer-exclude/log4j-1.2.8.jar:lib/installer-exclude/nist-sdp-1.0.jar:lib/installer-exclude/rome-0.9.jar:lib/installer-exclude/smack.jar:lib/installer-exclude/smackx.jar:lib/installer-exclude/Stun4J.jar:lib/installer-exclude/ymsg_network_v0_67.jar:lib/installer-exclude/fmj.jar:lib/installer-exclude/jna.jar:lib/installer-exclude/lti-civil-no_s_w_t.jar:lib/installer-exclude/swing-worker-1.2.jar:lib/os-specific/linux/installer-exclude/jmf.jar:lib/os-specific/linux/jdic_stub.jar:lib/os-specific/mac/OrangeExtensions.jar:lib/os-specific/mac/growl4j.jar:lib/os-specific/mac/jdic_stub.jar:lib/os-specific/mac/installer-exclude/jmf.jar:lib/os-specific/mac/installer-exclude/dock.jar:lib/os-specific/windows/jdic_stub.jar:lib/os-specific/windows/installer-exclude/jmf.jar:lib/os-specific/windows/installer-exclude/sound.jar:lib/installer-exclude/aclibico-2.1.jar:lib/installer-exclude/jdic_misc.jar:lib/installer-exclude/pircbot.jar:lib/os-specific/solaris/jdic_stub.jar:lib/os-specific/solaris/installer-exclude/jmf.jar:lib/installer-exclude/jsch-0.1.36.jar:lib/installer-exclude/apache-ant-1.7.0.jar:lib/installer-exclude/izpack-shortcut-link.jar:lib/installer-exclude/jfontchooser-1.0.5.jar:lib/installer-exclude/laf-widget.jar:lib/installer-exclude/transparency.jar:lib/installer-exclude/zrtp4j-light.jar:lib/installer-exclude/lcrypto-jdk16-143.jar:lib/installer-exclude/otr4j.jar:lib/installer-exclude/profiler4j-1.0-beta3-SC.jar:lib/installer-exclude/httpcore-4.0.1.jar:lib/installer-exclude/httpclient-4.0.1.jar:lib/installer-exclude/json-20090723.jar</classpath> <built-to>classes</built-to> <source-level>1.5</source-level> </compilation-unit> diff --git a/lib/installer-exclude/jml-1.0b2.jar b/lib/installer-exclude/jml-1.0b2.jar Binary files differdeleted file mode 100644 index 0f820f9..0000000 --- a/lib/installer-exclude/jml-1.0b2.jar +++ /dev/null diff --git a/lib/installer-exclude/jml-1.0b5.jar b/lib/installer-exclude/jml-1.0b5.jar Binary files differnew file mode 100644 index 0000000..0a8765a --- /dev/null +++ b/lib/installer-exclude/jml-1.0b5.jar diff --git a/resources/images/images.properties b/resources/images/images.properties index 4fc67e8..9fce8e2 100644 --- a/resources/images/images.properties +++ b/resources/images/images.properties @@ -180,6 +180,8 @@ service.gui.buttons.CALL_HISTORY_BUTTON=resources/images/impl/gui/buttons/callHi service.gui.buttons.CALL_HISTORY_BUTTON_PRESSED=resources/images/impl/gui/buttons/callHistoryButtonPressed.png service.gui.buttons.SEARCH_CALL_ICON=resources/images/impl/gui/buttons/searchCallIcon.png service.gui.buttons.SEARCH_CALL_ROLLOVER_ICON=resources/images/impl/gui/buttons/searchCallRolloverIcon.png +service.gui.buttons.ZOOM_OUT=resources/images/impl/gui/buttons/magnifier_zoom_out.png +service.gui.buttons.ZOOM_IN=resources/images/impl/gui/buttons/magnifier_zoom_in.png # Sound level icons service.gui.soundlevel.SOUND_LEVEL_ACTIVE=resources/images/impl/gui/common/soundlevel/soundActive.png diff --git a/resources/images/impl/gui/buttons/magnifier_zoom_in.png b/resources/images/impl/gui/buttons/magnifier_zoom_in.png Binary files differnew file mode 100644 index 0000000..af4fe07 --- /dev/null +++ b/resources/images/impl/gui/buttons/magnifier_zoom_in.png diff --git a/resources/images/impl/gui/buttons/magnifier_zoom_out.png b/resources/images/impl/gui/buttons/magnifier_zoom_out.png Binary files differnew file mode 100644 index 0000000..81f2819 --- /dev/null +++ b/resources/images/impl/gui/buttons/magnifier_zoom_out.png diff --git a/resources/languages/resources.properties b/resources/languages/resources.properties index e1a840b..ad0106d 100644 --- a/resources/languages/resources.properties +++ b/resources/languages/resources.properties @@ -483,6 +483,23 @@ service.gui.CONTINUE=Continue service.gui.SHOW_CERT=Show Certificate
service.gui.HIDE_CERT=Hide Certificate
+service.gui.avatar.CHOOSE_ICON=Choose icon ...
+service.gui.avatar.CLEAR_RECENT=Clear recent pictures
+service.gui.avatar.RECENT_ICONS=Recent icons:
+# service.gui.avatar.imagepicker
+service.gui.avatar.imagepicker.CANCEL=Cancel
+service.gui.avatar.imagepicker.CHOOSE_FILE=Choose ...
+service.gui.avatar.imagepicker.CLICK=Click and Smile
+service.gui.avatar.imagepicker.IMAGE_FILES=Image files
+service.gui.avatar.imagepicker.IMAGE_PICKER=Image picker
+service.gui.avatar.imagepicker.IMAGE_SIZE=Image size
+service.gui.avatar.imagepicker.INITIALIZING=Initializing
+service.gui.avatar.imagepicker.RESET=Reset
+service.gui.avatar.imagepicker.SET=Set
+service.gui.avatar.imagepicker.TAKE_PHOTO=Take a photo
+service.gui.avatar.imagepicker.WEBCAM_ERROR=Webcam error
+
+
# impl.protocol.ssh
#Contact Details Seperator(must not be part of contact data stored as persistent
# data)
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 9451094..c5a38b4 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 @@ -15,6 +15,7 @@ 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.service.gui.*; import net.java.sip.communicator.service.gui.Container; @@ -31,7 +32,8 @@ import net.java.sip.communicator.util.swing.*; public class AccountStatusPanel extends TransparentPanel implements RegistrationStateChangeListener, - PluginComponentListener + PluginComponentListener, + AvatarListener { /** * The desired height of the avatar. @@ -109,13 +111,16 @@ public class AccountStatusPanel if (ConfigurationManager.isTransparentWindowEnabled()) this.setUI(new SIPCommOpaquePanelUI()); - accountImageLabel - = new FramedImage( + FramedImageWithMenu imageWithMenu + = new FramedImageWithMenu( + mainFrame, new ImageIcon( ImageLoader .getImage(ImageLoader.DEFAULT_USER_PHOTO)), AVATAR_ICON_WIDTH, AVATAR_ICON_HEIGHT); + imageWithMenu.setPopupMenu(new SelectAvatarMenu(imageWithMenu)); + this.accountImageLabel = imageWithMenu; accountNameLabel.setFont( accountNameLabel.getFont().deriveFont(Font.BOLD)); @@ -261,6 +266,19 @@ public class AccountStatusPanel } /** + * Updates the image that is shown. + * @param img the new image. + */ + public void updateImage(ImageIcon img) + { + accountImageLabel.setImageIcon(img.getImage()); + accountImageLabel.setMaximumSize( + new Dimension(AVATAR_ICON_WIDTH, AVATAR_ICON_HEIGHT)); + revalidate(); + repaint(); + } + + /** * Starts connecting user interface for the given <tt>protocolProvider</tt>. * @param protocolProvider the <tt>ProtocolProviderService</tt> to start * connecting for @@ -391,6 +409,17 @@ public class AccountStatusPanel accountNameLabel.setText(accountName); } }.start(); + + OperationSetAvatar avatarOpSet + = protocolProvider.getOperationSet(OperationSetAvatar.class); + avatarOpSet.addAvatarListener(this); + } + else if (evt.getNewState().equals(RegistrationState.UNREGISTERING) + || evt.getNewState().equals(RegistrationState.CONNECTION_FAILED)) + { + OperationSetAvatar avatarOpSet + = protocolProvider.getOperationSet(OperationSetAvatar.class); + avatarOpSet.removeAvatarListener(this); } } @@ -475,4 +504,16 @@ public class AccountStatusPanel { return currentImage; } + + /** + * Called whenever a new avatar is defined for one of the protocols that we + * have subscribed for. + * + * @param event the event containing the new image + */ + public void avatarChanged(AvatarEvent event) + { + currentImage = event.getNewAvatar(); + accountImageLabel.setImageIcon(currentImage); + } }
\ No newline at end of file diff --git a/src/net/java/sip/communicator/impl/gui/main/presence/avatar/AvatarStackManager.java b/src/net/java/sip/communicator/impl/gui/main/presence/avatar/AvatarStackManager.java new file mode 100644 index 0000000..4cba88f --- /dev/null +++ b/src/net/java/sip/communicator/impl/gui/main/presence/avatar/AvatarStackManager.java @@ -0,0 +1,169 @@ +/* + * SIP Communicator, 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.presence.avatar; + +import java.awt.image.*; +import java.io.*; + +import javax.imageio.*; + +import net.java.sip.communicator.impl.gui.*; + +import net.java.sip.communicator.util.*; + +/** + * Take cares of storing(deleting, moving) images with the given indexes. + */ +public class AvatarStackManager +{ + /** + * The logger for this class. + */ + private static Logger logger = Logger.getLogger(AvatarStackManager.class); + + /** + * The folder where user avatars are stored. + */ + private static final String STORE_DIR = "avatarcache" + File.separator + + "userimages" + File.separator; + + /** + * Load the image at the defined index from user directory + * @param index the image index + * @return the image + */ + public static BufferedImage loadImage(int index) + { + String imagePath; + File imageFile; + + try + { + imagePath = STORE_DIR + index + ".png"; + + imageFile = GuiActivator.getFileAccessService() + .getPrivatePersistentFile(imagePath); + } + catch (Exception e) + { + // Unable to access stored image + System.err.println("Unable to access stored image as index " + index); + return null; + } + + // If the file don't exists, there is no more images to get + if (!imageFile.exists()) + return null; + + try + { + return ImageIO.read(imageFile); + } + catch (IOException ioe) + { + // Error while reading file + ioe.printStackTrace(System.err); + return null; + } + } + + /** + * Removes the oldest image and as its with lower index. + * Moves all indexes. Ant this way we free one index. + * @param nbImages + */ + public static void popFirstImage(int nbImages) + { + for (int i=nbImages-1; i>0; i--) + { + moveImage(i, i-1); + } + } + + /** + * Moves images. + * @param oldIndex the old index. + * @param newIndex the new index. + */ + private static void moveImage(int oldIndex, int newIndex) + { + String oldImagePath = STORE_DIR + oldIndex + ".png"; + String newImagePath = STORE_DIR + newIndex + ".png"; + + try + { + File oldFile = GuiActivator.getFileAccessService() + .getPrivatePersistentFile(oldImagePath); + + File newFile = GuiActivator.getFileAccessService() + .getPrivatePersistentFile(newImagePath); + + if (oldFile.exists()) + oldFile.renameTo(newFile); + } + catch (Exception e) + { + // TODO Auto-generated catch block + //e.printStackTrace(); + } + } + + /** + * Stores an image. + * @param image the image + * @param index of the image. + */ + public static void storeImage(BufferedImage image, int index) + { + String imagePath = STORE_DIR + index + ".png"; + + try + { + File storeDir = GuiActivator.getFileAccessService() + .getPrivatePersistentDirectory(STORE_DIR); + + // if dir doesn't exist create it + storeDir.mkdirs(); + + File file = GuiActivator.getFileAccessService() + .getPrivatePersistentFile(imagePath); + + ImageIO.write(image, "png", file); + } + catch (IOException e) + { + e.printStackTrace(); + // Error while writing + } + catch (Exception e) + { + // Unable to access image path + e.printStackTrace(); + } + } + + /** + * Deletes the stored image. + * @param index of the image to delete. + */ + public static void deleteImage(int index) + { + try + { + File imageFile = GuiActivator.getFileAccessService() + .getPrivatePersistentFile(STORE_DIR + index + ".png"); + + if (imageFile.exists() && !imageFile.delete()) + logger.error("Can't delete stored image at index " + index); + } + catch (Exception e) + { + // Can't access file + logger.info("Can't access to file: " + STORE_DIR + index + ".png"); + } + } +} diff --git a/src/net/java/sip/communicator/impl/gui/main/presence/avatar/SelectAvatarMenu.java b/src/net/java/sip/communicator/impl/gui/main/presence/avatar/SelectAvatarMenu.java new file mode 100644 index 0000000..aeea999 --- /dev/null +++ b/src/net/java/sip/communicator/impl/gui/main/presence/avatar/SelectAvatarMenu.java @@ -0,0 +1,353 @@ +/* + * SIP Communicator, 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.presence.avatar; + +import net.java.sip.communicator.impl.gui.*; +import net.java.sip.communicator.impl.gui.main.presence.avatar.imagepicker.*; +import net.java.sip.communicator.service.protocol.*; +import net.java.sip.communicator.util.*; +import net.java.sip.communicator.util.swing.*; + +import java.awt.*; +import java.awt.event.*; +import java.awt.image.*; +import java.util.*; + +import javax.swing.*; + +/** + * The dialog used as menu. + * + * @author Damian Minkov + */ +public class SelectAvatarMenu + extends JPopupMenu + implements ActionListener +{ + /** + * Logger for this class. + */ + private static final Logger logger = + Logger.getLogger(SelectAvatarMenu.class); + + /** + * Name of choose button. + */ + private static final String CHOSE_BUTTON_NAME = "chooseButton"; + + /** + * Name of clear button. + */ + private static final String CLEAR_BUTTON_NAME = "clearButton"; + + /** + * Images shown as history. + */ + private static final int MAX_STORED_IMAGES = 8; + + /** + * Ordered in columns. + */ + private static final int IMAGES_PER_COLUMN = 4; + + /** + * Thumbnail width. + */ + private static final int THUMB_WIDTH = 48; + + /** + * Thumbnail height. + */ + private static final int THUMB_HEIGHT = 48; + + /** + * Buttons corresponding to images. + */ + private SIPCommButton recentImagesButtons[] = + new SIPCommButton[MAX_STORED_IMAGES]; + + /** + * Next free image index number. + */ + private int nextImageIndex = 0; + + /** + * The parent button using us. + */ + private FramedImageWithMenu avatarImage; + + /** + * Creates the dialog. + * @param avatarImage the button that will trigger this menu. + */ + public SelectAvatarMenu(FramedImageWithMenu avatarImage) + { + this.avatarImage = avatarImage; + + init(); + + this.pack(); + } + + /** + * Init visible components. + */ + private void init() + { + TransparentPanel panel = new TransparentPanel(new BorderLayout()); + + panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + // Title label + JLabel titleLabel = new JLabel(GuiActivator.getResources() + .getI18NString("service.gui.avatar.RECENT_ICONS")); + titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD)); + panel.add(titleLabel, BorderLayout.NORTH); + + + // Init recent images grid + TransparentPanel recentImagesGrid = new TransparentPanel(); + recentImagesGrid.setLayout(new GridLayout(0, IMAGES_PER_COLUMN)); + + Dimension thumbsize = new Dimension(THUMB_WIDTH, THUMB_HEIGHT); + for (int i=0; i < MAX_STORED_IMAGES; i++) + { + this.recentImagesButtons[i] = new SIPCommButton(null); + this.recentImagesButtons[i].setBorder(BorderFactory.createEtchedBorder()); + this.recentImagesButtons[i].setMaximumSize(thumbsize); + this.recentImagesButtons[i].setMinimumSize(thumbsize); + this.recentImagesButtons[i].setPreferredSize(thumbsize); + this.recentImagesButtons[i].addActionListener(this); + this.recentImagesButtons[i].setName("" + i); + recentImagesGrid.add(this.recentImagesButtons[i]); + } + + panel.add(recentImagesGrid, BorderLayout.CENTER); + + // Action buttons + TransparentPanel buttonsPanel = new TransparentPanel(); + buttonsPanel.setLayout(new GridLayout(0, 1)); + + // we use this menu item just to get its foreground color. + Color linkColor = new JMenuItem().getForeground(); + + addActionButton(buttonsPanel, this, + GuiActivator.getResources().getI18NString( + "service.gui.avatar.CHOOSE_ICON"), + CHOSE_BUTTON_NAME, + linkColor); + addActionButton(buttonsPanel, this, + GuiActivator.getResources().getI18NString( + "service.gui.avatar.CLEAR_RECENT"), + CLEAR_BUTTON_NAME, + linkColor); + + panel.add(buttonsPanel, BorderLayout.SOUTH); + + this.setLayout(new BorderLayout()); + this.add(panel, BorderLayout.CENTER); + } + + /** + * Adds action buttons. + * @param buttonsPanel the panel to add to. + * @param listener the listener for actions + * @param text the text on the button. + * @param name name of the button. + * @param linkColor the color of the link. + */ + private static void addActionButton( + TransparentPanel buttonsPanel, ActionListener listener, + String text, String name, Color linkColor) + { + SIPCommLinkButton button = new SIPCommLinkButton(text); + button.setName(name); + button.addActionListener(listener); + button.setOpaque(false); + button.setLinkColor(linkColor); + + TransparentPanel panel = new TransparentPanel(new BorderLayout()); + panel.add(button, BorderLayout.WEST); + buttonsPanel.add(panel); + } + + @Override + public void setVisible(boolean b) + { + refreshRecentImages(); + super.setVisible(b); + } + + /** + * Refresh images with those stored locally. + */ + public void refreshRecentImages() + { + int i; + + for (i = 0; i < MAX_STORED_IMAGES; i++) + { + BufferedImage image = AvatarStackManager.loadImage(i); + if (image == null) + break; + + this.recentImagesButtons[i].setImage(createThumbnail(image)); + this.recentImagesButtons[i].setEnabled(true); + } + + if (i < MAX_STORED_IMAGES) + { + this.nextImageIndex = i; + + for (; i < MAX_STORED_IMAGES; i++) + { + this.recentImagesButtons[i].setImage(null); + this.recentImagesButtons[i].setEnabled(false); + } + } + else + this.nextImageIndex = MAX_STORED_IMAGES; + } + + /** + * Create thumbnail for the image. + * @param image to scale. + * @return the thumbnail image. + */ + private static BufferedImage createThumbnail(BufferedImage image) + { + int width = image.getWidth(); + int height = image.getHeight(); + + // Image smaller than the thumbnail size + if (width < THUMB_WIDTH && height < THUMB_HEIGHT) + return image; + + Image i; + + if (width > height) + i = image.getScaledInstance(THUMB_WIDTH, -1, Image.SCALE_SMOOTH); + else + i = image.getScaledInstance(-1, THUMB_HEIGHT, Image.SCALE_SMOOTH); + + return ImageUtils.getBufferedImage(i); + } + + /** + * Here is all the action. Stores the selected image into protocols and if + * needed update it ina AccountStatusPanel. + * + * @param image the new image. + */ + private void setNewImage(BufferedImage image) + { + AccountManager accountManager = GuiActivator.getAccountManager(); + + for(AccountID accountID : accountManager.getStoredAccounts()) + { + if(accountManager.isAccountLoaded(accountID)) + { + ProtocolProviderService protocolProvider + = GuiActivator.getRegisteredProviderForAccount(accountID); + + if(protocolProvider != null) + { + OperationSetAvatar opSetAvatar + = protocolProvider + .getOperationSet(OperationSetAvatar.class); + + if(opSetAvatar != null) + { + try + { + opSetAvatar.setAvatar( + ImageUtils.toByteArray(image)); + } + catch(Throwable t) + { + logger.error("Error setting image", t); + } + } + } + } + } + } + + /** + * Clear stored images. + */ + private void clearRecentImages() + { + for (int i=0; i < MAX_STORED_IMAGES; i++) + { + this.recentImagesButtons[i].setImage(null); + this.recentImagesButtons[i].setEnabled(false); + AvatarStackManager.deleteImage(i); + } + + this.nextImageIndex = 0; + } + + /** + * Action performed on various action links(buttons). + * + * @param e the action. + */ + public void actionPerformed(ActionEvent e) + { + JButton src = (JButton) e.getSource(); + + if (src instanceof SIPCommButton) + { + // Load image + int index = Integer.parseInt(src.getName()); + BufferedImage image = AvatarStackManager.loadImage(index); + + // Set the new image + setNewImage(image); + setVisible(false); + } + else if (src.getName().equals("chooseButton")) + { + // Open the image picker + Image currentImage = this.avatarImage.getAvatar(); + + ImagePickerDialog dialog = new ImagePickerDialog(96, 96); + + setVisible(false); + + byte[] bimage = dialog.showDialog(currentImage); + + if(bimage == null) + return; + + // New image + BufferedImage image = ImageUtils.getBufferedImage( + new ImageIcon(bimage).getImage()); + + // Store image + if (this.nextImageIndex == MAX_STORED_IMAGES) + { + // No more place to store images + // Pop the first element (index 0) + AvatarStackManager.popFirstImage(MAX_STORED_IMAGES); + + this.nextImageIndex = MAX_STORED_IMAGES - 1; + } + + // Store the new image on hard drive + AvatarStackManager.storeImage(image, this.nextImageIndex); + + // Inform protocols about the new image + setNewImage(image); + } + else if (src.getName().equals("clearButton")) + { + clearRecentImages(); + } + } +} diff --git a/src/net/java/sip/communicator/impl/gui/main/presence/avatar/imagepicker/EditPanel.java b/src/net/java/sip/communicator/impl/gui/main/presence/avatar/imagepicker/EditPanel.java new file mode 100644 index 0000000..6eae26e --- /dev/null +++ b/src/net/java/sip/communicator/impl/gui/main/presence/avatar/imagepicker/EditPanel.java @@ -0,0 +1,214 @@ +/* + * SIP Communicator, 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.presence.avatar.imagepicker; + +import java.awt.*; +import java.awt.image.*; + +import javax.swing.*; +import javax.swing.event.*; + +import net.java.sip.communicator.impl.gui.*; +import net.java.sip.communicator.util.*; +import net.java.sip.communicator.util.swing.*; + +/** + * The <tt>EditPanel</tt> manage the image size and the clipper component + * + * @author Damien Roth + * @author Shashank Tyagi + */ +public class EditPanel + extends TransparentPanel + implements ChangeListener +{ + private ImageClipper imageClipper; + private BufferedImage image; + private JSlider imageSizeSlider; + private JLabel zoomIn, zoomOut; + + private boolean resizeOnWidth = true; + private boolean smallImage = false; + + // Clipping zone dimension + private int clippingZoneWidth = 96; + private int clippingZoneHeight = 96; + + /** + * Create a new <tt>EditPanel</tt> + * + * @param clippingZoneWidth the width of the clipping zone + * @param clippingZoneHeight the height of the clipping zone + */ + public EditPanel(int clippingZoneWidth, int clippingZoneHeight) + { + super(); + this.setLayout(new BorderLayout()); + + this.clippingZoneWidth = clippingZoneWidth; + this.clippingZoneHeight = clippingZoneHeight; + + this.zoomOut = new JLabel(GuiActivator.getResources() + .getImage("service.gui.buttons.ZOOM_OUT")); + + this.zoomIn = new JLabel(GuiActivator.getResources() + .getImage("service.gui.buttons.ZOOM_IN")); + + imageSizeSlider = new JSlider(clippingZoneWidth, clippingZoneWidth, + clippingZoneWidth); + imageSizeSlider.addChangeListener(this); + imageSizeSlider.setOpaque(false); + imageSizeSlider.setToolTipText(GuiActivator.getResources() + .getI18NString("service.gui.avatar.imagepicker.IMAGE_SIZE")); + + TransparentPanel sliderPanel = new TransparentPanel(); + sliderPanel.add(this.zoomOut); + sliderPanel.add(this.imageSizeSlider); + sliderPanel.add(this.zoomIn); + + this.imageClipper = new ImageClipper(this.clippingZoneWidth, + this.clippingZoneHeight); + + this.add(imageClipper, BorderLayout.CENTER); + this.add(sliderPanel, BorderLayout.SOUTH); + } + + /** + * Sets the image to be edited + * + * @param image the image to be edited + */ + public void setImage(BufferedImage image) + { + int width = image.getWidth(null); + int height = image.getHeight(null); + + + // Checks if one dimension of the image is smaller than the clipping zone + if (width < this.clippingZoneWidth || height < this.clippingZoneHeight) + { + // Disable the slider used to set the size of the image + this.enableSlider(false); + this.smallImage = true; + + + /* Resize the image to match the clipping zone + * First case : + * * Image wider than high and the clipping zone + * * Image wider than high and included in the clipping zone + * Second case : + * * Image higher than wide and the clipping zone + * * Image wider than high and included in the clipping zone + */ + if ((width > height && width > this.clippingZoneWidth) + || (height > width && height <= this.clippingZoneHeight)) + { + this.image = ImageUtils.getBufferedImage( + image.getScaledInstance( + -1, this.clippingZoneHeight, Image.SCALE_SMOOTH)); + } + else + { + this.image = ImageUtils.getBufferedImage( + image.getScaledInstance( + this.clippingZoneWidth, -1, Image.SCALE_SMOOTH)); + } + } + else + { + this.image = image; + this.enableSlider(true); + this.resizeOnWidth = !(height < width); + this.imageSizeSlider.setMaximum(Math.min(width, height)); + } + + SwingUtilities.invokeLater(new Runnable() { + public void run() + { + reset(); + } + }); + } + + /** + * Reset the editor + */ + public void reset() + { + imageSizeSlider.setValue(this.imageSizeSlider.getMinimum()); + drawImage(); + } + + /** + * Returns the clipped image. + * + * @return + */ + public byte[] getClippedImage() + { + BufferedImage fullImage = getResizedImage(true); + + + Rectangle clipping = this.imageClipper.getClipping(); + + BufferedImage subImage = fullImage.getSubimage(clipping.x, clipping.y, + clipping.width, clipping.height); + + byte[] result = ImageUtils.toByteArray(subImage); + + return result; + } + + /** + * Resize the image. + * @param hq + * @return + */ + private BufferedImage getResizedImage(boolean hq) + { + BufferedImage i = null; + int size = this.imageSizeSlider.getValue(); + + if (this.resizeOnWidth) + { + i = ImageUtils.getBufferedImage( + this.image.getScaledInstance( + size, + -1, + (hq) ? Image.SCALE_SMOOTH : Image.SCALE_FAST)); + } + else + { + i = ImageUtils.getBufferedImage( + this.image.getScaledInstance( + -1, + size, + (hq) ? Image.SCALE_SMOOTH : Image.SCALE_FAST)); + } + + return i; + } + + private void drawImage() + { + // Use high quality scalling when the image is smaller than the clipper + this.imageClipper.setImage(getResizedImage(smallImage)); + } + + private void enableSlider(boolean enabled) + { + this.imageSizeSlider.setEnabled(enabled); + this.zoomIn.setEnabled(enabled); + this.zoomOut.setEnabled(enabled); + } + + public void stateChanged(ChangeEvent e) + { + // New size selected update the clipper + drawImage(); + } +} diff --git a/src/net/java/sip/communicator/impl/gui/main/presence/avatar/imagepicker/ImageClipper.java b/src/net/java/sip/communicator/impl/gui/main/presence/avatar/imagepicker/ImageClipper.java new file mode 100644 index 0000000..a25b82c --- /dev/null +++ b/src/net/java/sip/communicator/impl/gui/main/presence/avatar/imagepicker/ImageClipper.java @@ -0,0 +1,242 @@ +/* + * SIP Communicator, 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.presence.avatar.imagepicker; + +import java.awt.*; +import java.awt.event.*; +import java.awt.image.*; + +import javax.swing.*; + +import net.java.sip.communicator.util.swing.*; + +/** + * Component allowing the user to easily clip an image + * + * @author Damien Roth + */ +public class ImageClipper + extends JComponent + implements MouseListener, MouseMotionListener +{ + private static final int CLIP_PANEL_WIDTH = 320; + private static final int CLIP_PANEL_HEIGHT = 240; + + private static final Color IMAGE_BORDER_COLOR + = new Color(174, 189, 215); + private static final Color IMAGE_OVERLAY_COLOR + = new Color(1.0f, 1.0f, 1.0f, 0.4f); + + private BufferedImage image = null; + private Rectangle imageRect; + private Point imageBottomRight; + + private Rectangle clippingZoneRect; + private Point clippingZoneBottomRight; + + // Mouse drag vars + private int mouseStartX; + private int mouseStartY; + private int xInit; + private int yInit; + + /** + * Construct an new image clipper + * + * @param cropZoneWidth the width of the clip zone + * @param cropZoneHeight the height of the clip zone + */ + public ImageClipper(int cropZoneWidth, int cropZoneHeight) + { + Dimension d = new Dimension(CLIP_PANEL_WIDTH, CLIP_PANEL_HEIGHT); + + this.setSize(d); + this.setMaximumSize(d); + this.setMinimumSize(d); + this.setPreferredSize(d); + + this.initClippingZone(cropZoneWidth, cropZoneHeight); + this.imageRect = new Rectangle(this.clippingZoneRect.getLocation()); + this.imageBottomRight = new Point(0,0); + + this.addMouseListener(this); + this.addMouseMotionListener(this); + } + + /** + * Compute static values of the clipping zone + * + * @param width the width of the clipping zone + * @param height the height of the clipping zone + */ + private void initClippingZone(int width, int height) + { + this.clippingZoneRect = new Rectangle(width, height); + this.clippingZoneRect.x = (CLIP_PANEL_WIDTH / 2) - (width / 2); + this.clippingZoneRect.y = (CLIP_PANEL_HEIGHT / 2) - (height / 2); + + this.clippingZoneBottomRight = new Point( + this.clippingZoneRect.x + this.clippingZoneRect.width, + this.clippingZoneRect.y + this.clippingZoneRect.height + ); + } + + /** + * Defines the image to be clipped + * + * @param image the image to be clipped + */ + public void setImage(BufferedImage image) + { + boolean updated = false; + this.image = image; + + this.imageRect.width = image.getWidth(this); + this.imageRect.height = image.getHeight(this); + + this.imageBottomRight.x = this.imageRect.x + this.imageRect.width; + this.imageBottomRight.y = this.imageRect.y + this.imageRect.height; + + if (this.imageBottomRight.x < this.clippingZoneBottomRight.x) + { + this.imageRect.x += + this.clippingZoneBottomRight.x - this.imageBottomRight.x; + updated = true; + } + if (this.imageBottomRight.y < this.clippingZoneBottomRight.y) + { + this.imageRect.y += + this.clippingZoneBottomRight.y - this.imageBottomRight.y; + updated = true; + } + + if (updated) + { + this.imageBottomRight.x = this.imageRect.x + this.imageRect.width; + this.imageBottomRight.y = this.imageRect.y + this.imageRect.height; + } + + this.repaint(); + } + + /** + * Returns the clipped area of the image + * + * @return the clipped area + */ + public Rectangle getClipping() + { + Rectangle clipping = new Rectangle(); + + clipping.setSize(this.clippingZoneRect.getSize()); + clipping.x = this.clippingZoneRect.x - this.imageRect.x; + clipping.y = this.clippingZoneRect.y - this.imageRect.y; + + return clipping; + } + + @Override + protected void paintComponent(Graphics g) + { + super.paintComponent(g); + + g = g.create(); + AntialiasingManager.activateAntialiasing(g); + + // Draw image + g.drawImage(this.image, this.imageRect.x, this.imageRect.y, + this.imageRect.width, this.imageRect.height, this); + + // Image overlay + drawImageOverlay(g); + + // Image border + g.setColor(ImageClipper.IMAGE_BORDER_COLOR); + g.drawRoundRect(this.imageRect.x-2, this.imageRect.y-2, + this.imageRect.width+3, this.imageRect.height+3, 2, 2); + g.drawRoundRect(this.imageRect.x-1, this.imageRect.y-1, + this.imageRect.width+1, this.imageRect.height+1, 2, 2); + + // Select rect + g.setColor(Color.BLACK); + g.drawRect(this.clippingZoneRect.x, this.clippingZoneRect.y, + this.clippingZoneRect.width, this.clippingZoneRect.height); + } + + /** + * Draw an overlay over the parts of the images which are not in the clip zone + * + * @param g the Graphics used to draw + */ + private void drawImageOverlay(Graphics g) + { + int width, height; + + g.setColor(ImageClipper.IMAGE_OVERLAY_COLOR); + + width = this.clippingZoneRect.x - this.imageRect.x; + if (width > 0) + g.fillRect(this.imageRect.x, this.imageRect.y, + width, this.imageRect.height); + + width = this.imageRect.x + this.imageRect.width + - this.clippingZoneBottomRight.x; + if (width > 0) + g.fillRect(this.clippingZoneBottomRight.x, this.imageRect.y, + width, this.imageRect.height); + + // Top + height = this.clippingZoneRect.y - this.imageRect.y; + if (height > 0) + g.fillRect(this.clippingZoneRect.x, this.imageRect.y, + this.clippingZoneRect.width, height); + + // Bottom + height = (this.imageRect.y + this.imageRect.height) + - (this.clippingZoneBottomRight.y); + if (height > 0) + g.fillRect(this.clippingZoneRect.x, this.clippingZoneBottomRight.y, + this.clippingZoneRect.width, height); + } + + public void mousePressed(MouseEvent e) + { + // Init the dragging + mouseStartX = e.getX(); + mouseStartY = e.getY(); + xInit = this.imageRect.x; + yInit = this.imageRect.y; + } + + public void mouseDragged(MouseEvent e) + { + // New position of the image + int xpos = xInit + (e.getX() - mouseStartX); + int ypos = yInit + (e.getY() - mouseStartY); + + // Checks if the image doesn't go out of the clip zone + if (xpos <= this.clippingZoneRect.x && xpos + + this.imageRect.width > this.clippingZoneBottomRight.x) + this.imageRect.x = xpos; + + if (ypos <= this.clippingZoneRect.y && ypos + + this.imageRect.height > this.clippingZoneBottomRight.y) + this.imageRect.y = ypos; + + this.repaint(); + } + + public void mouseClicked(MouseEvent e) {} + + public void mouseEntered(MouseEvent e) {} + + public void mouseExited(MouseEvent e) {} + + public void mouseReleased(MouseEvent e) {} + + public void mouseMoved(MouseEvent e) {} +} diff --git a/src/net/java/sip/communicator/impl/gui/main/presence/avatar/imagepicker/ImagePickerDialog.java b/src/net/java/sip/communicator/impl/gui/main/presence/avatar/imagepicker/ImagePickerDialog.java new file mode 100644 index 0000000..2a1eb05 --- /dev/null +++ b/src/net/java/sip/communicator/impl/gui/main/presence/avatar/imagepicker/ImagePickerDialog.java @@ -0,0 +1,199 @@ +package net.java.sip.communicator.impl.gui.main.presence.avatar.imagepicker; + +import java.awt.*; +import java.awt.event.*; +import java.awt.image.*; +import java.io.*; + +import javax.imageio.*; +import javax.swing.*; + +import net.java.sip.communicator.impl.gui.*; +import net.java.sip.communicator.util.*; +import net.java.sip.communicator.util.swing.*; + +public class ImagePickerDialog + extends SIPCommDialog + implements ActionListener +{ + private TransparentPanel fullEditPanel; + + private EditPanel editPanel; + + private JButton okButton, cancelButton; + private JButton resetButton, selectFileButton, webcamButton; + + private boolean editCanceled = false; + + public ImagePickerDialog(int clipperZoneWidth, int clipperZoneHeight) + { + super(); + this.initComponents(clipperZoneWidth, clipperZoneHeight); + this.initDialog(); + + this.pack(); + this.setLocationRelativeTo(null); + } + + private void initDialog() + { + this.setTitle(GuiActivator.getResources() + .getI18NString("service.gui.avatar.imagepicker.IMAGE_PICKER")); + this.setModal(true); + this.setResizable(true); + + + this.setLayout(new BorderLayout()); + + TransparentPanel editButtonsPanel = new TransparentPanel(); + editButtonsPanel.setLayout(new GridLayout(1, 3, 5, 0)); + editButtonsPanel.add(this.selectFileButton); + editButtonsPanel.add(this.webcamButton); + editButtonsPanel.add(this.resetButton); + + this.fullEditPanel = new TransparentPanel(); + this.fullEditPanel.setLayout(new BorderLayout()); + this.fullEditPanel.add(this.editPanel, BorderLayout.CENTER); + this.fullEditPanel.add(editButtonsPanel, BorderLayout.SOUTH); + + TransparentPanel okCancelPanel = new TransparentPanel(); + okCancelPanel.setLayout(new FlowLayout(FlowLayout.RIGHT)); + okCancelPanel.add(cancelButton); + okCancelPanel.add(okButton); + + this.add(fullEditPanel, BorderLayout.CENTER); + this.add(okCancelPanel, BorderLayout.SOUTH); + + this.pack(); + + } + + private void initComponents(int clipperZoneWidth, int clipperZoneHeight) + { + // Edit panel + this.editPanel = new EditPanel(clipperZoneWidth, clipperZoneHeight); + + // Buttons + this.okButton = new JButton(GuiActivator.getResources() + .getI18NString("service.gui.avatar.imagepicker.SET")); + this.okButton.addActionListener(this); + this.okButton.setName("okButton"); + + this.cancelButton = new JButton(GuiActivator.getResources() + .getI18NString("service.gui.avatar.imagepicker.CANCEL")); + this.cancelButton.addActionListener(this); + this.cancelButton.setName("cancelButton"); + + this.resetButton = new JButton(GuiActivator.getResources() + .getI18NString("service.gui.avatar.imagepicker.RESET")); + this.resetButton.addActionListener(this); + this.resetButton.setName("resetButton"); + + this.selectFileButton = new JButton(GuiActivator.getResources() + .getI18NString("service.gui.avatar.imagepicker.CHOOSE_FILE")); + this.selectFileButton.addActionListener(this); + this.selectFileButton.setName("selectFileButton"); + + this.webcamButton = new JButton(GuiActivator.getResources() + .getI18NString("service.gui.avatar.imagepicker.TAKE_PHOTO")); + // disable it till we support it + this.webcamButton.setEnabled(false); + + this.webcamButton.addActionListener(this); + this.webcamButton.setName("webcamButton"); + } + + public byte[] showDialog(Image image) + { + if(image != null) + this.editPanel.setImage(ImageUtils.getBufferedImage(image)); + + this.setVisible(true); + + if (this.editCanceled) + return null; + else + return this.editPanel.getClippedImage(); + } + + public void actionPerformed(ActionEvent e) + { + String name = ((JButton) e.getSource()).getName(); + if (name.equals("cancelButton")) + { + editCanceled = true; + this.setVisible(false); + } + else if (name.equals("selectFileButton")) + { + SipCommFileChooser chooser = GenericFileDialog.create( + GuiActivator.getUIService().getMainFrame(), + GuiActivator.getResources().getI18NString( + "service.gui.avatar.imagepicker.CHOOSE_FILE"), + SipCommFileChooser.LOAD_FILE_OPERATION); + + chooser.addFilter(new ImageFileFilter()); + + File selectedFile = chooser.getFileFromDialog(); + if(selectedFile != null) + { + try + { + BufferedImage image = ImageIO.read(selectedFile); + this.editPanel.setImage(image); + } + catch (IOException ioe) + { + // TODO Auto-generated catch block + ioe.printStackTrace(); + } + } + } + else if (name.equals("okButton")) + { + editCanceled = false; + this.setVisible(false); + } + else if (name.equals("resetButton")) + { + this.editPanel.reset(); + } + else + { +// WebcamDialog dialog = new WebcamDialog(this); +// dialog.setVisible(true); +// byte[] bimage = dialog.getGrabbedImage(); +// +// if (bimage != null) +// { +// Image i = new ImageIcon(bimage).getImage(); +// editPanel.setImage(ImageUtils.getBufferedImage(i)); +// } + } + } + + protected void close(boolean isEscaped) + { + dispose(); + } + + class ImageFileFilter extends SipCommFileFilter + { + public boolean accept(File f) + { + String path = f.getAbsolutePath().toLowerCase(); + if (path.matches("(.*)\\.(jpg|jpeg|png|bmp)$") || + f.isDirectory()) + return true; + + else + return false; + } + + public String getDescription() + { + return GuiActivator.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/impl/gui/main/presence/avatar/imagepicker/WebcamDialog.java new file mode 100644 index 0000000..ede148e --- /dev/null +++ b/src/net/java/sip/communicator/impl/gui/main/presence/avatar/imagepicker/WebcamDialog.java @@ -0,0 +1,311 @@ +/* + * SIP Communicator, 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.presence.avatar.imagepicker; + +import java.awt.*; +import java.awt.event.*; + +import javax.swing.*; + +import net.java.sip.communicator.impl.gui.*; +import net.java.sip.communicator.service.audionotifier.*; +//import net.java.sip.communicator.service.media.*; +import net.java.sip.communicator.service.protocol.event.*; +import net.java.sip.communicator.util.swing.*; + +/** + * A dialog showing the webcam and allowing the user to grap a snapshot + * + * @author Damien Roth + */ +public class WebcamDialog + extends SIPCommDialog + implements ActionListener +// , VideoListener +{ + private VideoContainer videoContainer; + private Component videoComponent = null; + + private JButton cancelButton; + private JButton grabSnapshot; + + private byte[] grabbedImage = null; + + private TransparentPanel southPanel; + + private TransparentPanel timerPanel; + private TimerImage[] timerImages = new TimerImage[3]; + + /** + * Construct a <tt>WebcamDialog</tt> + * @param parent the ImagePickerDialog + */ + public WebcamDialog(ImagePickerDialog parent) + { + super(parent); + this.setTitle(GuiActivator.getResources() + .getI18NString("service.gui.avatar.imagepicker.TAKE_PHOTO")); + this.setModal(true); + + init(); + } + + /** + * Init the dialog + */ + private void init() + { + this.grabSnapshot = new JButton(); + this.grabSnapshot.setText(GuiActivator.getResources() + .getI18NString("service.gui.avatar.imagepicker.CLICK")); + this.grabSnapshot.setName("grab"); + this.grabSnapshot.addActionListener(this); + this.grabSnapshot.setEnabled(false); + + this.cancelButton = new JButton(GuiActivator.getResources() + .getI18NString("service.gui.avatar.imagepicker.CANCEL")); + this.cancelButton.setName("cancel"); + this.cancelButton.addActionListener(this); + + initAccessWebcam(); + + + // Timer Panel + this.timerPanel = new TransparentPanel(); + this.timerPanel.setLayout(new GridLayout(0, 3)); + + TransparentPanel tp; + for (int i = 0; i < this.timerImages.length; i++) + { + this.timerImages[i] = new TimerImage("" + (3-i)); + + tp = new TransparentPanel(); + tp.add(this.timerImages[i], BorderLayout.CENTER); + + this.timerPanel.add(tp); + } + + TransparentPanel buttonsPanel = new TransparentPanel(new GridLayout(1, 2)); + buttonsPanel.add(this.grabSnapshot); + buttonsPanel.add(this.cancelButton); + + // South Panel + this.southPanel = new TransparentPanel(new BorderLayout()); + this.southPanel.add(this.timerPanel, BorderLayout.CENTER); + this.southPanel.add(buttonsPanel, BorderLayout.SOUTH); + + this.add(this.videoContainer, BorderLayout.CENTER); + this.add(this.southPanel, BorderLayout.SOUTH); + } + + /** + * Init the access to the webcam (asynchonous call) + */ + private void initAccessWebcam() + { + Dimension d = new Dimension(320, 240); + + // Create a container for the video + this.videoContainer = new VideoContainer(new JLabel( + GuiActivator.getResources() + .getI18NString("service.gui.avatar.imagepicker.INITIALIZING"), + JLabel.CENTER)); + this.videoContainer.setPreferredSize(d); + this.videoContainer.setMinimumSize(d); + this.videoContainer.setMaximumSize(d); + +// try +// { +// // Call the method in the media service +// GuiActivator.getMediaService().createLocalVideoComponent(this); +// } catch (MediaException e) +// { +// //todo: In what scenarios are exceptions thrown and how to manage them? +// this.videoContainer = new VideoContainer(new JLabel( +// GuiActivator.getResources() +// .getI18NString("service.gui.avatar.imagepicker.WEBCAM_ERROR"))); +// e.printStackTrace(); +// } + } + + /** + * Grap the current image of the webcam throught the MediaService + */ + private void grabSnapshot() + { + // Just call the method "grabSnapshot" from the MediaService with the component +// try +// { +// this.grabbedImage = GuiActivator.getMediaService() +// .grabSnapshot(this.videoComponent); +// } +// catch (Exception e) +// { +// e.printStackTrace(); +// } + close(false); + this.setVisible(false); + } + + /** + * Return the grabbed snapshot as a byte array + * + * @return the grabbed snapshot + */ + public byte[] getGrabbedImage() + { + return this.grabbedImage; + } + + /** + * Play a snapshot sound + */ + private void playSound() + { + String soundKey = GuiActivator.getResources() + .getSoundPath("WEBCAM_SNAPSHOT"); + + SCAudioClip audio = GuiActivator.getAudioNotifier() + .createAudio(soundKey); + + audio.play(); + } + + protected void close(boolean isEscaped) + { +// try +// { +// if (this.videoComponent != null) +// { +// GuiActivator.getMediaService() +// .disposeLocalVideoComponent(this.videoComponent); +// this.videoComponent = null; +// } +// } +// catch (MediaException e) +// { +// // Better manager the exception ! +// e.printStackTrace(); +// } + } + + public void videoAdded(VideoEvent event) + { + // Here is the important part. With this event, you get the component + // containing the video ! + + // You need to keep it. The returned componant is the key for all + // the other methods ! + this.videoComponent = event.getVisualComponent(); + + // Add the component in the container + this.videoContainer.add(this.videoComponent); + + this.grabSnapshot.setEnabled(true); + } + + public void videoRemoved(VideoEvent event) + { + // In case of the video is removed elsewhere + this.videoComponent = null; + } + + public void actionPerformed(ActionEvent e) + { + String actName = ((JButton) e.getSource()).getName(); + + if (actName.equals("grab")) + { + if (this.videoComponent == null) + return; + + this.grabSnapshot.setEnabled(false); + new SnapshotTimer().start(); + } + else + { + close(false); + this.setVisible(false); + } + } + + private class SnapshotTimer + extends Thread + { + @Override + public void run() + { + int i; + + for (i=0; i < timerImages.length; i++) + { + timerImages[i].setElapsed(); + try + { + Thread.sleep(1000); + } + catch (InterruptedException e) + { + e.printStackTrace(); + } + } + + playSound(); + grabSnapshot(); + + WebcamDialog.this.setVisible(false); + WebcamDialog.this.dispose(); + + } + } + + private class TimerImage + extends JComponent + { + private static final int WIDTH = 30; + private static final int HEIGHT = 30; + + private boolean isElapsed = false; + private Font textFont = null; + private String second; + + public TimerImage(String second) + { + Dimension d = new Dimension(WIDTH, HEIGHT); + this.setPreferredSize(d); + this.setMinimumSize(d); + + + this.textFont = new Font("Sans", Font.BOLD, 20); + this.second = second; + } + + public void setElapsed() + { + this.isElapsed = true; + this.repaint(); + } + + @Override + protected void paintComponent(Graphics g) + { + Graphics2D g2d = (Graphics2D) g; + AntialiasingManager.activateAntialiasing(g); + + Color c = (isElapsed) + ? Color.RED + : new Color(150, 0, 0); + + g2d.setColor(c); + g2d.fillOval(0, 0, WIDTH, HEIGHT); + + g2d.setColor(Color.WHITE); + g2d.setFont(textFont); + g2d.drawString(this.second, 7, 21); + } + } +} 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 d9bd24c..0842e50 100644 --- a/src/net/java/sip/communicator/impl/gui/utils/ImageLoader.java +++ b/src/net/java/sip/communicator/impl/gui/utils/ImageLoader.java @@ -818,6 +818,18 @@ public class ImageLoader public static final ImageID CHAT_ROOM_CONFIG = new ImageID("service.gui.buttons.CHAT_ROOM_CONFIG"); + /** + * Zoom out Image for avatar panel + */ + public static final ImageID MAGNIFIER_ZOOM_OUT + = new ImageID("service.gui.buttons.ZOOM_OUT"); + + /** + * Zoom in Image for avatar panel + */ + public static final ImageID MAGNIFIER_ZOOM_IN + = new ImageID("service.gui.buttons.ZOOM_IN"); + /* * ======================================================================= * ------------------------ EDIT TOOLBAR ICONS --------------------------- diff --git a/src/net/java/sip/communicator/impl/protocol/icq/OperationSetAvatarIcqImpl.java b/src/net/java/sip/communicator/impl/protocol/icq/OperationSetAvatarIcqImpl.java new file mode 100644 index 0000000..068fd35 --- /dev/null +++ b/src/net/java/sip/communicator/impl/protocol/icq/OperationSetAvatarIcqImpl.java @@ -0,0 +1,26 @@ +/* + * SIP Communicator, 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.protocol.icq; + +import net.java.sip.communicator.service.protocol.*; + +/** + * A simple implementation of the <tt>OperationSetAvatar</tt> interface for the + * icq protocol. + * + * @author Damian Minkov + */ +public class OperationSetAvatarIcqImpl + extends AbstractOperationSetAvatar<ProtocolProviderServiceIcqImpl> +{ + public OperationSetAvatarIcqImpl( + ProtocolProviderServiceIcqImpl parentProvider, + OperationSetServerStoredAccountInfo accountInfoOpSet) + { + super(parentProvider, accountInfoOpSet, 0, 0, 0); + } +} diff --git a/src/net/java/sip/communicator/impl/protocol/icq/OperationSetPersistentPresenceIcqImpl.java b/src/net/java/sip/communicator/impl/protocol/icq/OperationSetPersistentPresenceIcqImpl.java index 1b9fcc2..eef5815 100644 --- a/src/net/java/sip/communicator/impl/protocol/icq/OperationSetPersistentPresenceIcqImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/icq/OperationSetPersistentPresenceIcqImpl.java @@ -1098,6 +1098,12 @@ public class OperationSetPersistentPresenceIcqImpl || evt.getNewState() == RegistrationState.AUTHENTICATION_FAILED || evt.getNewState() == RegistrationState.CONNECTION_FAILED) { + if(presenceQueryTimer != null) + { + presenceQueryTimer.cancel(); + presenceQueryTimer = null; + } + //since we are disconnected, we won't receive any further status //updates so we need to change by ourselves our own status as //well as set to offline all contacts in our contact list that 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 cfc304d..b30e130 100644 --- a/src/net/java/sip/communicator/impl/protocol/icq/OperationSetServerStoredAccountInfoIcqImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/icq/OperationSetServerStoredAccountInfoIcqImpl.java @@ -275,6 +275,24 @@ public class OperationSetServerStoredAccountInfoIcqImpl throw new ArrayIndexOutOfBoundsException( "Max count for this detail is already reached"); + if (detail instanceof ImageDetail) + { + if (iconListener == null) + { + iconListener = new IconUpdateListener(); + + this.icqProvider.getAimConnection().getExternalServiceManager(). + getIconServiceArbiter().addIconRequestListener( + new IconUpdateListener()); + } + + icqProvider.getAimConnection().getMyBuddyIconManager().requestSetIcon( + ByteBlock.wrap(((ServerStoredDetails.ImageDetail) detail).getBytes())); + infoRetreiver.detailsChanged(uin); + + return; + } + // everything is ok , so set it alreadySetDetails.add(detail); @@ -562,6 +580,26 @@ public class OperationSetServerStoredAccountInfoIcqImpl if(!isFound) return false; + //replacing in case of image + if (newDetailValue instanceof ImageDetail) + { + if (iconListener == null) + { + iconListener = new IconUpdateListener(); + + this.icqProvider.getAimConnection().getExternalServiceManager(). + getIconServiceArbiter().addIconRequestListener( + new IconUpdateListener()); + } + icqProvider.getAimConnection().getMyBuddyIconManager() + .requestSetIcon(ByteBlock.wrap( + ((ServerStoredDetails.ImageDetail) newDetailValue) + .getBytes())); + + infoRetreiver.detailsChanged(uin); + return true; + } + SuccessResponseListener responseListener = new SuccessResponseListener(); // // if toBeCleared == null. make it empty one diff --git a/src/net/java/sip/communicator/impl/protocol/icq/ProtocolProviderServiceIcqImpl.java b/src/net/java/sip/communicator/impl/protocol/icq/ProtocolProviderServiceIcqImpl.java index c896864..3cbe5da 100644 --- a/src/net/java/sip/communicator/impl/protocol/icq/ProtocolProviderServiceIcqImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/icq/ProtocolProviderServiceIcqImpl.java @@ -497,12 +497,23 @@ public class ProtocolProviderServiceIcqImpl infoRetreiver, this)); - addSupportedOperationSet( - OperationSetServerStoredAccountInfo.class, - new OperationSetServerStoredAccountInfoIcqImpl( + OperationSetServerStoredAccountInfoIcqImpl + serverStoredAccountInfoOpSet = + new OperationSetServerStoredAccountInfoIcqImpl( infoRetreiver, screenname, - this)); + this); + addSupportedOperationSet( + OperationSetServerStoredAccountInfo.class, + serverStoredAccountInfoOpSet); + +// Currently disabled as when we send avatar +// we receive an error from server +// addSupportedOperationSet( +// OperationSetAvatar.class, +// new OperationSetAvatarIcqImpl( +// this, +// serverStoredAccountInfoOpSet)); addSupportedOperationSet( OperationSetWebAccountRegistration.class, diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/ChatRoomJabberImpl.java b/src/net/java/sip/communicator/impl/protocol/jabber/ChatRoomJabberImpl.java index 280fffb..6bda38d 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/ChatRoomJabberImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/ChatRoomJabberImpl.java @@ -1626,6 +1626,9 @@ public class ChatRoomJabberImpl newMessage, messageReceivedEventType); + if(delay != null) + msgReceivedEvt.setHistoryMessage(true); + fireMessageEvent(msgReceivedEvt); } } 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 e6e9d35..5998c20 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetServerStoredAccountInfoJabberImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/OperationSetServerStoredAccountInfoJabberImpl.java @@ -6,9 +6,11 @@ */ package net.java.sip.communicator.impl.protocol.jabber; -import java.util.Iterator; -import java.util.List; -import java.util.Vector; +import java.util.*; + +import org.jivesoftware.smack.*; +import org.jivesoftware.smackx.packet.*; + import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.service.protocol.ServerStoredDetails.*; @@ -182,7 +184,45 @@ public class OperationSetServerStoredAccountInfoJabberImpl 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()); + //} + + Iterator iter = getDetails(detail.getClass()); + int currentDetailsSize = 0; + while (iter.hasNext()) { + currentDetailsSize++; + } + if (currentDetailsSize >= getMaxDetailInstances(detail.getClass())) + { + throw new ArrayIndexOutOfBoundsException( + "Max count for this detail is already reached"); + } + + if(detail instanceof ImageDetail) + { + try + { + VCard v1 = new VCard(); + v1.load(jabberProvider.getConnection()); + + v1.setAvatar(((ImageDetail) detail).getBytes()); + + v1.save(jabberProvider.getConnection()); + } catch (XMPPException xmppe) + { + xmppe.printStackTrace(); + } + } } /** @@ -221,6 +261,55 @@ public class OperationSetServerStoredAccountInfoJabberImpl ServerStoredDetails.GenericDetail newDetailValue) throws ClassCastException, OperationFailedException { + assertConnected(); + + if (!newDetailValue.getClass().equals(currentDetailValue.getClass())) + { + throw new ClassCastException( + "New value to be replaced is not as the current one"); + } + // if values are the same no change + if (currentDetailValue.equals(newDetailValue)) + { + return true; + } + + boolean isFound = false; + Iterator iter = + infoRetreiver.getDetails(uin, currentDetailValue.getClass()); + + while (iter.hasNext()) + { + GenericDetail item = (GenericDetail) iter.next(); + if (item.equals(currentDetailValue)) + { + isFound = true; + break; + } + } + // current detail value does not exist + if (!isFound) + { + return false; + } + + if(newDetailValue instanceof ImageDetail) + { + try + { + VCard v1 = new VCard(); + v1.load(jabberProvider.getConnection()); + + v1.setAvatar(((ImageDetail) newDetailValue).getBytes()); + v1.save(jabberProvider.getConnection()); + + return true; + } catch (XMPPException xmppe) + { + xmppe.printStackTrace(); + } + } + return false; } diff --git a/src/net/java/sip/communicator/impl/protocol/msn/ContactGroupMsnImpl.java b/src/net/java/sip/communicator/impl/protocol/msn/ContactGroupMsnImpl.java index 9834b2b..5217d35 100644 --- a/src/net/java/sip/communicator/impl/protocol/msn/ContactGroupMsnImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/msn/ContactGroupMsnImpl.java @@ -84,7 +84,7 @@ public class ContactGroupMsnImpl for (MsnContact groupMember : groupMembers) { /* - *Only add the contact if it doesn't already exist in some other + * Only add the contact if it doesn't already exist in some other * group. This is necessary because MSN would allow having one and * the same contact in more than one group. */ diff --git a/src/net/java/sip/communicator/impl/protocol/msn/MsnActivator.java b/src/net/java/sip/communicator/impl/protocol/msn/MsnActivator.java index dc765e1..e29c598 100644 --- a/src/net/java/sip/communicator/impl/protocol/msn/MsnActivator.java +++ b/src/net/java/sip/communicator/impl/protocol/msn/MsnActivator.java @@ -9,9 +9,11 @@ package net.java.sip.communicator.impl.protocol.msn; import java.util.*; import net.java.sip.communicator.service.configuration.*; +import net.java.sip.communicator.service.fileaccess.*; import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.service.resources.*; +import net.java.sip.communicator.util.*; import org.osgi.framework.*; /** @@ -27,6 +29,7 @@ public class MsnActivator private ServiceRegistration msnPpFactoryServReg = null; private static BundleContext bundleContext = null; private static ConfigurationService configurationService = null; + private static FileAccessService fileAccessService = null; private static ProtocolProviderFactoryMsnImpl msnProviderFactory = null; @@ -144,4 +147,21 @@ public class MsnActivator = ResourceManagementServiceUtils.getService(bundleContext); return resources; } + + /** + * Returns the <tt>FileAccessService</tt> obtained from the bundle context. + * + * @return the <tt>FileAccessService</tt> obtained from the bundle context + */ + public static FileAccessService getFileAccessService() + { + if (fileAccessService == null) + { + fileAccessService + = ServiceUtils.getService( + bundleContext, + FileAccessService.class); + } + return fileAccessService; + } } diff --git a/src/net/java/sip/communicator/impl/protocol/msn/OperationSetAvatarMsnImpl.java b/src/net/java/sip/communicator/impl/protocol/msn/OperationSetAvatarMsnImpl.java new file mode 100644 index 0000000..d65389d --- /dev/null +++ b/src/net/java/sip/communicator/impl/protocol/msn/OperationSetAvatarMsnImpl.java @@ -0,0 +1,26 @@ +/* + * SIP Communicator, 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.protocol.msn; + +import net.java.sip.communicator.service.protocol.*; + +/** + * A simple implementation of the <tt>OperationSetAvatar</tt> interface for the + * msn protocol. + * + * @author Damian Minkov + */ +public class OperationSetAvatarMsnImpl + extends AbstractOperationSetAvatar<ProtocolProviderServiceMsnImpl> +{ + public OperationSetAvatarMsnImpl( + ProtocolProviderServiceMsnImpl parentProvider, + OperationSetServerStoredAccountInfo accountInfoOpSet) + { + super(parentProvider, accountInfoOpSet, 0, 0, 0); + } +} diff --git a/src/net/java/sip/communicator/impl/protocol/msn/OperationSetServerStoredAccountInfoMsnImpl.java b/src/net/java/sip/communicator/impl/protocol/msn/OperationSetServerStoredAccountInfoMsnImpl.java new file mode 100644 index 0000000..014ce4c --- /dev/null +++ b/src/net/java/sip/communicator/impl/protocol/msn/OperationSetServerStoredAccountInfoMsnImpl.java @@ -0,0 +1,553 @@ +/* + * SIP Communicator, 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.protocol.msn; + +import java.io.*; +import java.util.*; +import net.java.sip.communicator.service.protocol.*; +import net.java.sip.communicator.service.protocol.ServerStoredDetails.*; +import net.java.sip.communicator.service.protocol.event.*; +import net.java.sip.communicator.util.*; +import net.sf.jml.*; + +import javax.imageio.*; + +/** + * Saves account avatar image. If one is already saved we set it as initial one + * for the MsnOwner. + * + * @author SR + * @author Damian Minkov + */ +public class OperationSetServerStoredAccountInfoMsnImpl + implements OperationSetServerStoredAccountInfo, + RegistrationStateChangeListener +{ + /** + * Logger for this class. + */ + private static final Logger logger = + Logger.getLogger(OperationSetServerStoredAccountInfoMsnImpl.class); + + /** + * The jabber provider that created us. + */ + private ProtocolProviderServiceMsnImpl msnProvider = null; + /** + * Our account uin=email address. + */ + private String uin = null; + + /** + * A place to store our own picture. + */ + private static final String STORE_DIR = "avatarcache" + File.separator; + + /** + * Here is kept all the details retrieved so far. + */ + private Hashtable<String,List<GenericDetail>> retrievedDetails + = new Hashtable<String,List<GenericDetail>>(); + + protected OperationSetServerStoredAccountInfoMsnImpl( + ProtocolProviderServiceMsnImpl msnProvider, + String uin) + { + this.msnProvider = msnProvider; + this.uin = uin; + + this.msnProvider.addRegistrationStateChangeListener(this); + } + + /** + * Returns an iterator over all details that are instances or descendants of + * the specified class. If for example an our account has a work address + * and an address detail, a call to this method with AddressDetail.class + * would return both of them. + * <p> + * @param detailClass one of the detail classes defined in the + * ServerStoredDetails class, indicating the kind of details we're + * interested in. + * <p> + * @return a java.util.Iterator over all details that are instances or + * descendants of the specified class. + */ + public Iterator<GenericDetail> getDetailsAndDescendants(Class detailClass) + { + assertConnected(); + + List<GenericDetail> details = getContactDetails(uin); + List<GenericDetail> result = new LinkedList<GenericDetail>(); + + Iterator<GenericDetail> iter = details.iterator(); + while (iter.hasNext()) + { + GenericDetail item = iter.next(); + if (detailClass.isInstance(item)) + { + result.add(item); + } + } + + return result.iterator(); + } + + /** + * request the full info for the given contactAddress + * waits and return this details + * + * @param contactAddress String + * @return Vector the details + */ + List<GenericDetail> getContactDetails(String contactAddress) + { + List<GenericDetail> result = retrievedDetails.get(contactAddress); + + if (result == null) + { + result = new LinkedList<GenericDetail>(); + try + { + MsnMessenger messenger = msnProvider.getMessenger(); + + if (messenger == null) + { + return null; + } + + Email email = Email.parseStr(contactAddress); + + String tmp = null; + byte[] imageBytes; + if (messenger.getOwner().getEmail().equals(email)) + { + MsnOwner owner = messenger.getOwner(); + tmp = owner.getDisplayName(); + result.add(new ServerStoredDetails.DisplayNameDetail(tmp)); + + MsnObject image = owner.getDisplayPicture(); + if (image != null) + { + imageBytes = image.getMsnObj(); + if (imageBytes != null && imageBytes.length > 0) + { + result.add(new ServerStoredDetails.ImageDetail( + "Image", imageBytes)); + } + } + } else + { + MsnContact contact = + messenger.getContactList().getContactByEmail(email); + tmp = contact.getDisplayName(); + result.add(new ServerStoredDetails.DisplayNameDetail(tmp)); + imageBytes = contact.getAvatar().getMsnObj(); + if (imageBytes != null && imageBytes.length > 0) + { + result.add(new ServerStoredDetails.ImageDetail( + "Image", imageBytes)); + } + } + + } catch (Exception exc) + { + logger.error("Cannot load details for contact " + this + + " : " + exc.getMessage(), exc); + } + } + + retrievedDetails.put(contactAddress, result); + + return new LinkedList<GenericDetail>(result); + } + + /** + * Returns an iterator over all details that are instances of exactly the + * same class as the one specified. Not that, contrary to the + * getDetailsAndDescendants() method this one would only return details + * that are instances of the specified class and not only its descendants. + * If for example our account has both a work address and an address detail, + * a call to this method with AddressDetail.class would return only the + * AddressDetail instance and not the WorkAddressDetail instance. + * <p> + * @param detailClass one of the detail classes defined in the + * ServerStoredDetails class, indicating the kind of details we're + * interested in. + * <p> + * @return a java.util.Iterator over all details of specified class. + */ + public Iterator<GenericDetail> getDetails(Class detailClass) + { + assertConnected(); + + return getDetails(uin, detailClass); + } + + /** + * Returns all details currently available and set for our account. + * <p> + * @return a java.util.Iterator over all details currently set our account. + */ + public Iterator<GenericDetail> getAllAvailableDetails() + { + assertConnected(); + + return getContactDetails(uin).iterator(); + } + + /** + * Returns all detail Class-es that the underlying implementation supports + * setting. Note that if you call one of the modification methods (add + * remove or replace) with a detail not contained by the iterator returned + * by this method, an IllegalArgumentException will be thrown. + * <p> + * @return a java.util.Iterator over all detail classes supported by the + * implementation. + */ + public Iterator<Class<? extends GenericDetail>> getSupportedDetailTypes() + { + List<GenericDetail> details = getContactDetails(uin); + List<Class<? extends GenericDetail>> result + = new LinkedList<Class<? extends GenericDetail>>(); + + Iterator<GenericDetail> iter = details.iterator(); + while (iter.hasNext()) + { + GenericDetail obj = iter.next(); + result.add(obj.getClass()); + } + + return result.iterator(); + } + + /** + * Determines whether a detail class represents a detail supported by the + * underlying implementation or not. Note that if you call one of the + * modification methods (add remove or replace) with a detail that this + * method has determined to be unsupported (returned false) this would lead + * to an IllegalArgumentException being thrown. + * <p> + * @param detailClass the class the support for which we'd like to + * determine. + * <p> + * @return true if the underlying implementation supports setting details of + * this type and false otherwise. + */ + public boolean isDetailClassSupported( + Class<? extends GenericDetail> detailClass) + { + List<GenericDetail> details = getContactDetails(uin); + Iterator<GenericDetail> iter = details.iterator(); + while (iter.hasNext()) + { + GenericDetail obj = iter.next(); + if (detailClass.isAssignableFrom(obj.getClass())) + { + return true; + } + } + return false; + } + + /** + * The method returns the number of instances supported for a particular + * detail type. Some protocols offer storing multiple values for a + * particular detail type. Spoken languages are a good example. + * @param detailClass the class whose max instance number we'd like to find + * out. + * <p> + * @return int the maximum number of detail instances. + */ + public int getMaxDetailInstances(Class detailClass) + { + return 1; + } + + /** + * returns the user details from the specified class + * exactly that class not its descendants + * + * @param uin String + * @param detailClass Class + * @return Iterator + */ + private Iterator<GenericDetail> getDetails(String uin, Class detailClass) + { + List<GenericDetail> details = getContactDetails(uin); + List<GenericDetail> result = new LinkedList<GenericDetail>(); + + Iterator<GenericDetail> iter = details.iterator(); + while (iter.hasNext()) + { + GenericDetail item = iter.next(); + if (detailClass.equals(item.getClass())) + { + result.add(item); + } + } + + return result.iterator(); + } + + /** + * Adds the specified detail to the list of details registered on-line + * 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 + * also thrown in case the class of the specified detail is not supported by + * the underlying implementation, i.e. its class name was not returned by the + * getSupportedDetailTypes() method. + * <p> + * @param detail the detail that we'd like registered on the server. + * <p> + * @throws IllegalArgumentException if such a detail already exists and its + * 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 provied the list of classes that currently have data associatd 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()); +// } + + Iterator iter = getDetails(detail.getClass()); + int currentDetailsSize = 0; + while (iter.hasNext()) + { + currentDetailsSize++; + } + if (currentDetailsSize >= getMaxDetailInstances(detail.getClass())) { + throw new ArrayIndexOutOfBoundsException( + "Max count for this detail is already reached"); + } + + MsnOwner owner = msnProvider.getMessenger().getOwner(); + + if (detail instanceof ImageDetail) + { + try + { + String path = storePicture(((ImageDetail) detail).getBytes()); + + FileInputStream in = new FileInputStream(path); + byte[] b = new byte[in.available()]; + in.read(b); + in.close(); + + owner.setDisplayPicture(MsnObject.getInstance( + owner.getEmail().getEmailAddress(), + b)); + } catch(Exception e) + { + logger.error("Error setting own avatar.", e); + } + } + } + + /** + * Stores the picture. + * @param data data to store + * @return the picture path. + * @throws Exception + */ + private String storePicture(byte[] data) + throws Exception + { + String imagePath = STORE_DIR + + msnProvider.getAccountID().getAccountUniqueID() + ".jpg"; + + File storeDir = MsnActivator.getFileAccessService() + .getPrivatePersistentDirectory(STORE_DIR); + + // if dir doesn't exist create it + storeDir.mkdirs(); + + File file = MsnActivator.getFileAccessService() + .getPrivatePersistentFile(imagePath); + + ImageIO.write( + ImageIO.read(new ByteArrayInputStream(data)), + "jpg", + file); + + return file.getPath(); + } + + /** + * 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. + * <p> + * @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; + } + + /** + * Replaces the currentDetailValue detail with newDetailValue and returns + * true if the operation was a success or false if currentDetailValue did + * not previously exist (in this case an additional call to addDetail is + * required). + * <p> + * @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. + * @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 + { + assertConnected(); + + if (!newDetailValue.getClass().equals(currentDetailValue.getClass())) + { + throw new ClassCastException("New value to be replaced is not " + + "as the current one"); + } + // if values are the same no change + if (currentDetailValue.equals(newDetailValue)) + { + return true; + } + + boolean isFound = false; + Iterator iter = getDetails(uin, currentDetailValue.getClass()); + while (iter.hasNext()) + { + GenericDetail item = (GenericDetail) iter.next(); + if (item.equals(currentDetailValue)) + { + isFound = true; + break; + + } + } + // current detail value does not exist + if (!isFound) + { + return false; + } + + MsnOwner owner = msnProvider.getMessenger().getOwner(); + + if (newDetailValue instanceof ImageDetail) + { + try + { + String path = storePicture( + ((ImageDetail) newDetailValue).getBytes()); + + FileInputStream in = new FileInputStream(path); + byte[] b = new byte[in.available()]; + in.read(b); + in.close(); + + owner.setDisplayPicture(MsnObject.getInstance( + owner.getEmail().getEmailAddress(), + b)); + + return true; + } catch(Exception e) + { + logger.error("Error setting own avatar.", e); + } + } + + return false; + } + + /** + * Utility method throwing an exception if the icq stack is not properly + * initialized. + * @throws java.lang.IllegalStateException if the underlying ICQ stack is + * not registered and initialized. + */ + private void assertConnected() + throws IllegalStateException + { + if (msnProvider == null) + { + throw new IllegalStateException( + "The jabber provider must be non-null and signed on " + + "before being able to communicate."); + } + + if (!msnProvider.isRegistered()) + { + throw new IllegalStateException( + "The jabber provider must be signed on before " + + "being able to communicate."); + } + } + + /** + * The method is called by a <code>ProtocolProviderService</code> + * implementation whenever a change in the registration state of the + * corresponding provider had occurred. + * + * @param evt the event describing the status change. + */ + public void registrationStateChanged(RegistrationStateChangeEvent evt) + { + if(evt.getNewState() == RegistrationState.REGISTERING) + { + try + { + String imagePath = STORE_DIR + + msnProvider.getAccountID().getAccountUniqueID() + ".jpg"; + + File file = MsnActivator.getFileAccessService() + .getPrivatePersistentFile(imagePath); + + if(file.exists()) + { + FileInputStream in = new FileInputStream(file); + byte[] b = new byte[in.available()]; + in.read(b); + in.close(); + + MsnOwner owner = msnProvider.getMessenger().getOwner(); + + owner.setInitDisplayPicture(MsnObject.getInstance( + owner.getEmail().getEmailAddress(), + b)); + } + } + catch(Exception ex) + { + logger.error("Cannot obtain own avatar image.", ex); + } + } + } +} diff --git a/src/net/java/sip/communicator/impl/protocol/msn/ProtocolProviderServiceMsnImpl.java b/src/net/java/sip/communicator/impl/protocol/msn/ProtocolProviderServiceMsnImpl.java index c46861a..7bc61f7 100644 --- a/src/net/java/sip/communicator/impl/protocol/msn/ProtocolProviderServiceMsnImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/msn/ProtocolProviderServiceMsnImpl.java @@ -211,6 +211,12 @@ public class ProtocolProviderServiceMsnImpl persistentPresence.setMessenger(messenger); typingNotifications.setMessenger(messenger); + fireRegistrationStateChanged( + getRegistrationState(), + RegistrationState.REGISTERING, + RegistrationStateChangeEvent.REASON_NOT_SPECIFIED, + null); + try { messenger.login(); @@ -306,6 +312,17 @@ public class ProtocolProviderServiceMsnImpl OperationSetPresence.class, persistentPresence); + //initialize AccountInfo + OperationSetServerStoredAccountInfoMsnImpl accountInfo + = new OperationSetServerStoredAccountInfoMsnImpl( + this, screenname); + addSupportedOperationSet( + OperationSetServerStoredAccountInfo.class, + accountInfo); + addSupportedOperationSet( + OperationSetAvatar.class, + new OperationSetAvatarMsnImpl(this, accountInfo)); + addSupportedOperationSet( OperationSetAdHocMultiUserChat.class, new OperationSetAdHocMultiUserChatMsnImpl(this)); diff --git a/src/net/java/sip/communicator/impl/protocol/msn/msn.provider.manifest.mf b/src/net/java/sip/communicator/impl/protocol/msn/msn.provider.manifest.mf index 5f33c55..dd8be41 100755 --- a/src/net/java/sip/communicator/impl/protocol/msn/msn.provider.manifest.mf +++ b/src/net/java/sip/communicator/impl/protocol/msn/msn.provider.manifest.mf @@ -28,7 +28,9 @@ Import-Package: org.apache.commons.logging, javax.xml.parsers, javax.xml.datatype, sun.security.action, + javax.imageio, net.java.sip.communicator.service.configuration, + net.java.sip.communicator.service.fileaccess, net.java.sip.communicator.service.resources, net.java.sip.communicator.util, net.java.sip.communicator.service.configuration.event, diff --git a/src/net/java/sip/communicator/service/protocol/event/ChatRoomMessageReceivedEvent.java b/src/net/java/sip/communicator/service/protocol/event/ChatRoomMessageReceivedEvent.java index d980d92..0fdd67f 100644 --- a/src/net/java/sip/communicator/service/protocol/event/ChatRoomMessageReceivedEvent.java +++ b/src/net/java/sip/communicator/service/protocol/event/ChatRoomMessageReceivedEvent.java @@ -64,6 +64,11 @@ public class ChatRoomMessageReceivedEvent private final int eventType; /** + * Some services can fill our room with message history. + */ + private boolean historyMessage = false; + + /** * Creates a <tt>MessageReceivedEvent</tt> representing reception of the * <tt>source</tt> message received from the specified <tt>from</tt> * contact. @@ -138,4 +143,23 @@ public class ChatRoomMessageReceivedEvent { return eventType; } + + /** + * Is current event for history message. + * @return is current event for history message. + */ + public boolean isHistoryMessage() + { + return historyMessage; + } + + /** + * Changes property, whether this event is for a history message. + * + * @param historyMessage whether its event for history message. + */ + public void setHistoryMessage(boolean historyMessage) + { + this.historyMessage = historyMessage; + } } diff --git a/src/net/java/sip/communicator/util/ImageUtils.java b/src/net/java/sip/communicator/util/ImageUtils.java index 9b7d7d0..4d17686 100644 --- a/src/net/java/sip/communicator/util/ImageUtils.java +++ b/src/net/java/sip/communicator/util/ImageUtils.java @@ -259,4 +259,52 @@ public class ImageUtils } return image; } + + /** + * Returns the buffered image corresponding to the given image + * @param source an image + * @return the buffered image corresponding to the given image + */ + public static BufferedImage getBufferedImage(Image source) + { + if (source == null) + { + return null; + } + else if (source instanceof BufferedImage) + { + return (BufferedImage) source; + } + + int width = source.getWidth(null); + int height = source.getHeight(null); + + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics graphics = image.createGraphics(); + graphics.drawImage(source, 0, 0, null); + graphics.dispose(); + + return image; + } + + /** + * Extracts bytes from image. + * @param image the image. + * @return the bytes of the image. + */ + public static byte[] toByteArray(BufferedImage image) + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try + { + ImageIO.write(image, "png", out); + } + catch (IOException e) + { + logger.debug("Cannot convert buffered image to byte[]", e); + return null; + } + + return out.toByteArray(); + } } diff --git a/src/net/java/sip/communicator/util/swing/FramedImage.java b/src/net/java/sip/communicator/util/swing/FramedImage.java index d74ff77..86db4d2 100644 --- a/src/net/java/sip/communicator/util/swing/FramedImage.java +++ b/src/net/java/sip/communicator/util/swing/FramedImage.java @@ -24,9 +24,9 @@ public class FramedImage private ImageIcon icon; - private final int width; + protected final int width; - private final int height; + protected final int height; /** * Creates a FramedImage by specifying the width and the height of the @@ -85,6 +85,31 @@ public class FramedImage } /** + * Sets the image to display in the frame. + * + * @param image the image to display in the frame + */ + public void setImageIcon(Image image) + { + icon = ImageUtils.getScaledRoundedIcon(image, width - 2, height - 2); + + if (this.isVisible()) + { + this.revalidate(); + this.repaint(); + } + } + + /** + * Returns the image that is shown. + * @return + */ + public Image getImage() + { + return icon.getImage(); + } + + /** * Paints the contained image in a frame. * * Overrides {@link JComponent#paintComponent(Graphics)}. diff --git a/src/net/java/sip/communicator/util/swing/FramedImageWithMenu.java b/src/net/java/sip/communicator/util/swing/FramedImageWithMenu.java new file mode 100644 index 0000000..90a1eca --- /dev/null +++ b/src/net/java/sip/communicator/util/swing/FramedImageWithMenu.java @@ -0,0 +1,274 @@ +/* + * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.util.swing; + +import java.awt.*; +import java.awt.event.*; + +import javax.swing.*; +import javax.swing.event.*; + +import org.jvnet.lafwidget.animation.*; + +/** + * A custom component, used to show images in a frame. A rollover for the + * content image and optional menu in dialog. + * + * @author Damien Roth + */ +public class FramedImageWithMenu + extends FramedImage + implements MouseListener, PopupMenuListener +{ + /** + * The dialog containing the menu with actions. + */ + private JPopupMenu popupMenu; + + /** + * The parent frame. + */ + private JFrame mainFrame; + + /** + * Should we currently draw overlay. + */ + private boolean drawOverlay = false; + + /** + * Are we showing custom image or the default one. + */ + private boolean isDefaultImage = true; + + /** + * The current image. + */ + private Image currentImage; + + /** + * Creates the component. + * @param mainFrame the parent frame. + * @param imageIcon the image icon to show as default one. + * @param width width of component. + * @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); + } + + /** + * Sets the dialog used for menu for this Image. + * @param popupMenu the dialog to show as menu. Can be null if no menu + * will be available. + */ + public void setPopupMenu(JPopupMenu popupMenu) + { + this.popupMenu = popupMenu; + if(popupMenu != null) + this.popupMenu.addPopupMenuListener(this); + } + + /** + * Sets the image to display in the frame. + * + * @param imageIcon the image to display in the frame + */ + public void setImageIcon(ImageIcon imageIcon) + { + // Intercept the action to validate the user icon and not the default + super.setImageIcon(imageIcon.getImage()); + this.isDefaultImage = false; + + this.currentImage = imageIcon.getImage(); + } + + /** + * Returns the current image with no rounded corners. Only return the user + * image and not the default image. + * + * @return the current image - null if it's the default image + */ + public Image getAvatar() + { + return (!this.isDefaultImage) ? this.currentImage : this.getImage(); + } + + @Override + public void paintComponent(Graphics g) + { + super.paintComponent(g); + + if (drawOverlay) + { + g = g.create(); + AntialiasingManager.activateAntialiasing(g); + + try + { + // Paint a roll over fade out. + FadeTracker fadeTracker = FadeTracker.getInstance(); + + float visibility = 0.0f; + if (fadeTracker.isTracked(this, FadeKind.ROLLOVER)) + { + visibility = fadeTracker.getFade(this, FadeKind.ROLLOVER); + visibility /= 4; + } + else + visibility = 0.5f; + + // Draw black overlay + g.setColor(new Color(0.0f, 0.0f, 0.0f, visibility)); + g.fillRoundRect(1, 1, width - 2, height - 2, 10, 10); + + // Draw arrow + g.setColor(Color.WHITE); + + int[] arrowX = new int[] { + width - 17, + width - 7, + width - 12 + }; + int[] arrowY = new int[] { + height - 12, + height - 12, + height - 7 + }; + g.fillPolygon(arrowX, arrowY, arrowX.length); + } + finally + { + g.dispose(); + } + } + } + + /** + * Show the avatar dialog as a glasspane of the mainframe + * + * @param show show dialogs if sets to TRUE - hide otherwise + */ + private void showDialog(MouseEvent e, boolean show) + { + if (this.popupMenu == null) + { + return; + } + + if (show) + { + Point imageLoc = this.getLocationOnScreen(); + Point rootPaneLoc = mainFrame.getRootPane().getLocationOnScreen(); + + this.popupMenu.setSize(mainFrame.getRootPane().getWidth(), + this.popupMenu.getHeight()); + + this.popupMenu.show(this, (rootPaneLoc.x - imageLoc.x), + this.getHeight()); + } + else + { + this.drawOverlay = false; + this.repaint(); + } + } + + public void mouseEntered(MouseEvent e) + { + if (this.drawOverlay) + return; + + this.drawOverlay = true; + + FadeTracker fadeTracker = FadeTracker.getInstance(); + + fadeTracker.trackFadeIn(FadeKind.ROLLOVER, + FramedImageWithMenu.this, + true, + new AvatarRepaintCallback()); + } + + public void mouseExited(MouseEvent e) + { + // Remove overlay only if the dialog isn't visible + if (!popupMenu.isVisible()) + { + this.drawOverlay = false; + this.repaint(); + } + } + + public void mouseReleased(MouseEvent e) + { + showDialog(e, !popupMenu.isVisible()); + } + + /** + * This method is called before the popup menu becomes visible + */ + public void popupMenuWillBecomeVisible(PopupMenuEvent e) {} + + /** + * This method is called before the popup menu becomes invisible + * Note that a JPopupMenu can become invisible any time + */ + public void popupMenuWillBecomeInvisible(PopupMenuEvent e) + { + this.drawOverlay = false; + this.repaint(); + } + + /** + * This method is called when the popup menu is canceled + */ + public void popupMenuCanceled(PopupMenuEvent e){} + + /** + * The <tt>ButtonRepaintCallback</tt> is charged to repaint this button + * when the fade animation is performed. + */ + private class AvatarRepaintCallback + implements FadeTrackerCallback + { + public void fadeEnded(FadeKind arg0) + { + repaintLater(); + } + + public void fadePerformed(FadeKind arg0, float arg1) + { + repaintLater(); + } + + private void repaintLater() + { + SwingUtilities.invokeLater(new Runnable() + { + public void run() + { + FramedImageWithMenu.this.repaint(); + } + }); + } + + public void fadeReversed(FadeKind arg0, boolean arg1, float arg2) + { + } + } + + public void mouseClicked(MouseEvent e) {} + + public void mousePressed(MouseEvent e) {} +} diff --git a/src/net/java/sip/communicator/util/swing/SIPCommLinkButton.java b/src/net/java/sip/communicator/util/swing/SIPCommLinkButton.java new file mode 100644 index 0000000..ae43b4f --- /dev/null +++ b/src/net/java/sip/communicator/util/swing/SIPCommLinkButton.java @@ -0,0 +1,276 @@ +/* + * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.util.swing; + +import net.java.sip.communicator.util.swing.plaf.*; + +import java.awt.*; +import java.net.*; + +import javax.swing.*; + +/** + * A button which text is a link. The button looks like a link. + */ +public class SIPCommLinkButton + extends JButton +{ + private static final long serialVersionUID = 1L; + + private static final String UIClassID = "LinkButtonUI"; + + public static final int ALWAYS_UNDERLINE = 0; + + public static final int HOVER_UNDERLINE = 1; + + public static final int NEVER_UNDERLINE = 2; + + private int linkBehavior; + + private Color linkColor; + + private Color colorPressed; + + private Color visitedLinkColor; + + private Color disabledLinkColor; + + private URL buttonURL; + + private boolean isLinkVisited; + + /** + * Created Link Button. + */ + public SIPCommLinkButton() + { + this(null, null); + } + + /** + * Created Link Button with text. + * @param text + */ + public SIPCommLinkButton(String text) + { + this(text, null); + } + + /** + * Created Link Button with url. + * @param url + */ + public SIPCommLinkButton(URL url) + { + this(null, url); + } + + /** + * Created Link Button with text and url. + * @param text + * @param url + */ + public SIPCommLinkButton(String text, URL url) + { + super(text); + + // Define UI + this.setUI(SIPCommLinkButtonUI.createUI(this)); + UIManager.getDefaults().put("LinkButtonUI", "SIPCommLinkButtonUI"); + linkBehavior = SIPCommLinkButton.HOVER_UNDERLINE; + + linkColor = Color.blue; + colorPressed = Color.red; + visitedLinkColor = new Color(128, 0, 128); + + if (text == null && url != null) + this.setText(url.toExternalForm()); + setLinkURL(url); + + this.setBorderPainted(false); + this.setContentAreaFilled(false); + this.setRolloverEnabled(true); + this.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } + + @Override + public void updateUI() + { + this.setUI(SIPCommLinkButtonUI.createUI(this)); + } + + public String getUIClassID() + { + return SIPCommLinkButton.UIClassID; + } + + /** + * Setup the tooltip. + */ + protected void setupToolTipText() + { + String tip = null; + if (buttonURL != null) + tip = buttonURL.toExternalForm(); + setToolTipText(tip); + } + + /** + * Changes link behaviour. + * @param bnew the new behaviour. One of ALWAYS_UNDERLINE, HOVER_UNDERLINE + * and NEVER_UNDERLINE. + */ + public void setLinkBehavior(int bnew) + { + if (bnew != ALWAYS_UNDERLINE && bnew != HOVER_UNDERLINE + && bnew != NEVER_UNDERLINE) + throw new IllegalArgumentException("Not a legal LinkBehavior"); + + int old = linkBehavior; + linkBehavior = bnew; + firePropertyChange("linkBehavior", old, bnew); + repaint(); + } + + /** + * Returns the link behaviour. + * @return the link behaviour. + */ + public int getLinkBehavior() + { + return linkBehavior; + } + + /** + * Sets the link color. + * @param color the new color. + */ + public void setLinkColor(Color color) + { + Color colorOld = linkColor; + linkColor = color; + firePropertyChange("linkColor", colorOld, color); + repaint(); + } + + /** + * Return the link color. + * @return link color. + */ + public Color getLinkColor() + { + return linkColor; + } + + /** + * Sets the active link color. + * @param colorNew the new color. + */ + public void setActiveLinkColor(Color colorNew) + { + Color colorOld = colorPressed; + colorPressed = colorNew; + firePropertyChange("activeLinkColor", colorOld, colorNew); + repaint(); + } + + /** + * Returns the active link color. + * @return the active link color. + */ + public Color getActiveLinkColor() + { + return colorPressed; + } + + /** + * Sets disabled link color. + * @param color the new color. + */ + public void setDisabledLinkColor(Color color) + { + Color colorOld = disabledLinkColor; + disabledLinkColor = color; + firePropertyChange("disabledLinkColor", colorOld, color); + if (!isEnabled()) + repaint(); + } + + /** + * Returns the disabled link color. + * @return the disabled link color. + */ + public Color getDisabledLinkColor() + { + return disabledLinkColor; + } + + /** + * Set visited link color. + * @param colorNew the new visited link color. + */ + public void setVisitedLinkColor(Color colorNew) + { + Color colorOld = visitedLinkColor; + visitedLinkColor = colorNew; + firePropertyChange("visitedLinkColor", colorOld, colorNew); + repaint(); + } + + /** + * Returns visited link color. + * @return visited link color. + */ + public Color getVisitedLinkColor() + { + return visitedLinkColor; + } + + /** + * Set a link. + * @param url the url. + */ + public void setLinkURL(URL url) + { + URL urlOld = buttonURL; + buttonURL = url; + setupToolTipText(); + firePropertyChange("linkURL", urlOld, url); + revalidate(); + repaint(); + } + + /** + * Returns the url. + * @return the link url. + */ + public URL getLinkURL() + { + return buttonURL; + } + + /** + * Set a link visited. + * @param flagNew is link visited. + */ + public void setLinkVisited(boolean flagNew) + { + boolean flagOld = isLinkVisited; + isLinkVisited = flagNew; + firePropertyChange("linkVisited", flagOld, flagNew); + repaint(); + } + + /** + * Returns is link visited. + * @return is link visited. + */ + public boolean isLinkVisited() + { + return isLinkVisited; + } +} diff --git a/src/net/java/sip/communicator/util/swing/plaf/SIPCommLinkButtonUI.java b/src/net/java/sip/communicator/util/swing/plaf/SIPCommLinkButtonUI.java new file mode 100644 index 0000000..1885bf6 --- /dev/null +++ b/src/net/java/sip/communicator/util/swing/plaf/SIPCommLinkButtonUI.java @@ -0,0 +1,75 @@ +/* + * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.util.swing.plaf; + +import net.java.sip.communicator.util.swing.*; + +import java.awt.*; + +import javax.swing.*; +import javax.swing.plaf.*; +import javax.swing.plaf.basic.*; + +/** + * The SIPCommLinkButtonUI implementation. + * @author ROTH Damien + */ +public class SIPCommLinkButtonUI + extends BasicButtonUI +{ + private static final SIPCommLinkButtonUI ui = new SIPCommLinkButtonUI(); + + public static ComponentUI createUI(JComponent jcomponent) + { + return ui; + } + + protected void paintText( + Graphics g, JComponent com, Rectangle rect, String s) + { + SIPCommLinkButton bn = (SIPCommLinkButton) com; + + ButtonModel bnModel = bn.getModel(); + if (bnModel.isEnabled()) + { + if (bnModel.isPressed()) + bn.setForeground(bn.getActiveLinkColor()); + else if (bn.isLinkVisited()) + bn.setForeground(bn.getVisitedLinkColor()); + else + bn.setForeground(bn.getLinkColor()); + } + else + { + if (bn.getDisabledLinkColor() != null) + bn.setForeground(bn.getDisabledLinkColor()); + } + + super.paintText(g, com, rect, s); + int behaviour = bn.getLinkBehavior(); + + if (!(behaviour == SIPCommLinkButton.HOVER_UNDERLINE + && bnModel.isRollover()) + && behaviour != SIPCommLinkButton.ALWAYS_UNDERLINE) + return; + + FontMetrics fm = g.getFontMetrics(); + int x = rect.x + getTextShiftOffset(); + int y = (rect.y + fm.getAscent() + + fm.getDescent() + getTextShiftOffset()) - 1; + if (bnModel.isEnabled()) + { + g.setColor(bn.getForeground()); + g.drawLine(x, y, (x + rect.width) - 1, y); + } + else + { + g.setColor(bn.getBackground().brighter()); + g.drawLine(x, y, (x + rect.width) - 1, y); + } + } +} |