Java tutorial
/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.java.sip.communicator.impl.gui.main.chat; import java.awt.*; import java.awt.Container; import java.awt.event.*; import java.io.*; import java.util.*; import java.util.List; import javax.swing.*; import javax.swing.Timer; import javax.swing.event.*; import javax.swing.text.*; import javax.swing.text.html.*; import javax.swing.undo.*; import net.java.sip.communicator.impl.gui.*; import net.java.sip.communicator.impl.gui.event.*; import net.java.sip.communicator.impl.gui.main.chat.conference.*; import net.java.sip.communicator.impl.gui.main.chat.menus.*; import net.java.sip.communicator.impl.gui.utils.*; import net.java.sip.communicator.plugin.desktoputil.*; import net.java.sip.communicator.service.contactlist.*; import net.java.sip.communicator.service.contactsource.*; import net.java.sip.communicator.service.gui.*; import net.java.sip.communicator.service.gui.event.*; import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.service.resources.*; import net.java.sip.communicator.util.*; import net.java.sip.communicator.util.skin.*; import org.apache.commons.lang3.*; import org.jitsi.service.configuration.*; import org.osgi.framework.*; /** * The <tt>ChatWritePanel</tt> is the panel, where user writes her messages. * It is located at the bottom of the split in the <tt>ChatPanel</tt> and it * contains an editor, where user writes the text. * * @author Yana Stamcheva * @author Lyubomir Marinov * @author Adam Netocny */ public class ChatWritePanel extends TransparentPanel implements ActionListener, KeyListener, MouseListener, UndoableEditListener, DocumentListener, PluginComponentListener, Skinnable, ChatSessionChangeListener { /** * The <tt>Logger</tt> used by the <tt>ChatWritePanel</tt> class and its * instances for logging output. */ private static final Logger logger = Logger.getLogger(ChatWritePanel.class); private final JEditorPane editorPane = new JEditorPane(); private final UndoManager undo = new UndoManager(); private final ChatPanel chatPanel; private final Timer stoppedTypingTimer = new Timer(2 * 1000, this); private final Timer typingTimer = new Timer(5 * 1000, this); private int typingState = OperationSetTypingNotifications.STATE_STOPPED; private WritePanelRightButtonMenu rightButtonMenu; private final ArrayList<ChatMenuListener> menuListeners = new ArrayList<ChatMenuListener>(); private final SIPCommScrollPane scrollPane = new SIPCommScrollPane(); private ChatTransportSelectorBox transportSelectorBox; private final Container centerPanel; private SIPCommToggleButton smsButton; private JLabel smsCharCountLabel; private JLabel smsNumberLabel; private int smsNumberCount = 1; private int smsCharCount = 160; private boolean smsMode = false; /** * Mode where we do not mix sending im and sms in one chat window. */ private boolean disableMergedSmsMode = false; /** * Property to control merge sms mode. */ private static final String MERGE_SMS_MODE_DISABLED_PROP = "net.java.sip.communicator.impl.gui.MERGE_SMS_MODE_DISABLED"; /** * A timer used to reset the transport resource to the bare ID if there was * no activity from this resource since a bunch of time. */ private java.util.Timer outdatedResourceTimer = null; /** * Tells if the current resource is outdated. A timer has already been * triggered, but when there is only a single resource there is no bare ID * available. Thus, flag this resource as outdated to switch to the bare ID * when available. */ private boolean isOutdatedResource = true; /** * List of plugin components that are registered for updates. */ private List<PluginComponent> pluginComponents = Collections.synchronizedList(new ArrayList<PluginComponent>()); /** * Creates an instance of <tt>ChatWritePanel</tt>. * * @param panel The parent <tt>ChatPanel</tt>. */ public ChatWritePanel(ChatPanel panel) { super(new BorderLayout()); this.chatPanel = panel; centerPanel = createCenter(); int chatAreaSize = ConfigurationUtils.getChatWriteAreaSize(); Dimension writeMessagePanelDefaultSize = new Dimension(500, (chatAreaSize > 0) ? chatAreaSize : 28); Dimension writeMessagePanelMinSize = new Dimension(500, 28); Dimension writeMessagePanelMaxSize = new Dimension(500, 100); setMinimumSize(writeMessagePanelMinSize); setMaximumSize(writeMessagePanelMaxSize); setPreferredSize(writeMessagePanelDefaultSize); this.add(centerPanel, BorderLayout.CENTER); this.rightButtonMenu = new WritePanelRightButtonMenu(chatPanel.getChatContainer()); this.typingTimer.setRepeats(true); // initialize send command to Ctrl+Enter ConfigurationService configService = GuiActivator.getConfigurationService(); disableMergedSmsMode = configService.getBoolean(MERGE_SMS_MODE_DISABLED_PROP, disableMergedSmsMode); String messageCommandProperty = "service.gui.SEND_MESSAGE_COMMAND"; String messageCommand = configService.getString(messageCommandProperty); if (messageCommand == null) messageCommand = GuiActivator.getResources().getSettingsString(messageCommandProperty); this.changeSendCommand((messageCommand == null || messageCommand.equalsIgnoreCase("enter"))); if (ConfigurationUtils.isFontSupportEnabled()) initDefaultFontConfiguration(); } /** * Initializes the default font configuration for this chat write area. */ private void initDefaultFontConfiguration() { String fontFamily = ConfigurationUtils.getChatDefaultFontFamily(); int fontSize = ConfigurationUtils.getChatDefaultFontSize(); // Font family and size if (fontFamily != null && fontSize > 0) setFontFamilyAndSize(fontFamily, fontSize); // Font style setBoldStyleEnable(ConfigurationUtils.isChatFontBold()); setItalicStyleEnable(ConfigurationUtils.isChatFontItalic()); setUnderlineStyleEnable(ConfigurationUtils.isChatFontUnderline()); // Font color Color fontColor = ConfigurationUtils.getChatDefaultFontColor(); if (fontColor != null) setFontColor(fontColor); } /** * Creates the center panel. * * @return the created center panel */ private Container createCenter() { JPanel centerPanel = new JPanel(new GridBagLayout()); centerPanel.setBackground(Color.WHITE); centerPanel.setBorder(BorderFactory.createEmptyBorder(3, 0, 3, 3)); GridBagConstraints constraints = new GridBagConstraints(); initSmsLabel(centerPanel); initTextArea(centerPanel); smsCharCountLabel = new JLabel(String.valueOf(smsCharCount)); smsCharCountLabel.setForeground(Color.GRAY); smsCharCountLabel.setVisible(false); constraints.anchor = GridBagConstraints.NORTHEAST; constraints.fill = GridBagConstraints.NONE; constraints.gridx = 3; constraints.gridy = 0; constraints.weightx = 0f; constraints.weighty = 0f; constraints.insets = new Insets(0, 2, 0, 2); constraints.gridheight = 1; constraints.gridwidth = 1; centerPanel.add(smsCharCountLabel, constraints); smsNumberLabel = new JLabel(String.valueOf(smsNumberCount)) { @Override public void paintComponent(Graphics g) { AntialiasingManager.activateAntialiasing(g); g.setColor(getBackground()); g.fillOval(0, 0, getWidth(), getHeight()); super.paintComponent(g); } }; smsNumberLabel.setHorizontalAlignment(JLabel.CENTER); smsNumberLabel.setPreferredSize(new Dimension(18, 18)); smsNumberLabel.setMinimumSize(new Dimension(18, 18)); smsNumberLabel.setForeground(Color.WHITE); smsNumberLabel.setBackground(Color.GRAY); smsNumberLabel.setVisible(false); constraints.anchor = GridBagConstraints.NORTHEAST; constraints.fill = GridBagConstraints.NONE; constraints.gridx = 4; constraints.gridy = 0; constraints.weightx = 0f; constraints.weighty = 0f; constraints.insets = new Insets(0, 2, 0, 2); constraints.gridheight = 1; constraints.gridwidth = 1; centerPanel.add(smsNumberLabel, constraints); return centerPanel; } /** * Initializes the sms menu. * * @param centerPanel the parent panel */ private void initSmsLabel(final JPanel centerPanel) { GridBagConstraints constraints = new GridBagConstraints(); constraints.anchor = GridBagConstraints.NORTHWEST; constraints.fill = GridBagConstraints.NONE; constraints.gridx = 1; constraints.gridy = 0; constraints.gridheight = 1; constraints.weightx = 0f; constraints.weighty = 0f; constraints.insets = new Insets(0, 3, 0, 0); ImageID smsIcon = new ImageID("service.gui.icons.SEND_SMS"); ImageID selectedIcon = new ImageID("service.gui.icons.SEND_SMS_SELECTED"); smsButton = new SIPCommToggleButton(ImageLoader.getImage(smsIcon), ImageLoader.getImage(selectedIcon), ImageLoader.getImage(smsIcon), ImageLoader.getImage(selectedIcon)); smsButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (smsMode && !isIMAllowed()) { return; } smsMode = smsButton.isSelected(); Color bgColor; if (smsMode) { bgColor = new Color(GuiActivator.getResources().getColor("service.gui.LIST_SELECTION_COLOR")); smsCharCountLabel.setVisible(true); smsNumberLabel.setVisible(true); } else { bgColor = Color.WHITE; smsCharCountLabel.setVisible(false); smsNumberLabel.setVisible(false); } centerPanel.setBackground(bgColor); editorPane.setBackground(bgColor); } }); // We hide the sms label until we know if the chat supports sms. smsButton.setVisible(false); centerPanel.add(smsButton, constraints); } private void initTextArea(JPanel centerPanel) { GridBagConstraints constraints = new GridBagConstraints(); editorPane.setContentType("text/html"); editorPane.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE); editorPane.setCaretPosition(0); editorPane.setEditorKit(new SIPCommHTMLEditorKit(this)); editorPane.getDocument().addUndoableEditListener(this); editorPane.getDocument().addDocumentListener(this); editorPane.addKeyListener(this); editorPane.addMouseListener(this); editorPane.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); editorPane.setDragEnabled(true); editorPane.setTransferHandler(new ChatTransferHandler(chatPanel)); scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); scrollPane.setOpaque(false); scrollPane.getViewport().setOpaque(false); scrollPane.setBorder(null); scrollPane.setViewportView(editorPane); constraints.anchor = GridBagConstraints.NORTHWEST; constraints.fill = GridBagConstraints.BOTH; constraints.gridx = 2; constraints.gridy = 0; constraints.weightx = 1f; constraints.weighty = 1f; constraints.gridheight = 1; constraints.gridwidth = 1; constraints.insets = new Insets(0, 0, 0, 0); centerPanel.add(scrollPane, constraints); } /** * Runs clean-up for associated resources which need explicit disposal (e.g. * listeners keeping this instance alive because they were added to the * model which operationally outlives this instance). */ public void dispose() { /* * Stop the Timers because they're implicitly globally referenced and * thus don't let them retain this instance. */ typingTimer.stop(); typingTimer.removeActionListener(this); stoppedTypingTimer.stop(); stoppedTypingTimer.removeActionListener(this); if (typingState != OperationSetTypingNotifications.STATE_STOPPED) stopTypingTimer(); if (outdatedResourceTimer != null) { outdatedResourceTimer.cancel(); outdatedResourceTimer.purge(); outdatedResourceTimer = null; } editorPane.removeKeyListener(this); menuListeners.clear(); if (rightButtonMenu != null) { rightButtonMenu.dispose(); rightButtonMenu = null; } scrollPane.dispose(); } /** * Returns the editor panel, contained in this <tt>ChatWritePanel</tt>. * * @return The editor panel, contained in this <tt>ChatWritePanel</tt>. */ public JEditorPane getEditorPane() { return editorPane; } /** * Replaces the Ctrl+Enter send command with simple Enter. * * @param isEnter indicates if the new send command is enter or cmd-enter */ public void changeSendCommand(boolean isEnter) { ActionMap actionMap = editorPane.getActionMap(); actionMap.put("send", new SendMessageAction()); actionMap.put("newLine", new NewLineAction()); InputMap im = this.editorPane.getInputMap(); if (isEnter) { im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "send"); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.CTRL_DOWN_MASK), "newLine"); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.SHIFT_DOWN_MASK), "newLine"); this.setToolTipText("<html>" + GuiActivator.getResources().getI18NString("service.gui.SEND_MESSAGE") + " - Enter <br> " + "Use Ctrl-Enter or Shift-Enter to make a new line" + "</html>"); } else { im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.CTRL_DOWN_MASK), "send"); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "newLine"); this.setToolTipText( GuiActivator.getResources().getI18NString("service.gui.SEND_MESSAGE") + " Ctrl-Enter"); } } /** * Enables/disables the sms mode. * * @param selected <tt>true</tt> to enable sms mode, <tt>false</tt> - * otherwise */ public void setSmsSelected(boolean selected) { if (disableMergedSmsMode && isIMAllowed()) return; if ((selected && !smsButton.isSelected()) || (!selected && smsButton.isSelected())) { smsButton.doClick(); } } /** * Returns <tt>true</tt> if the sms mode is enabled, otherwise returns * <tt>false</tt>. * @return <tt>true</tt> if the sms mode is enabled, otherwise returns * <tt>false</tt> */ public boolean isSmsSelected() { return smsMode; } /** * Checks if sending IM message is allowed. When in sms mode, it * can be the only method to send message. We will disable sms -> im * switching. * @return is IM allowed. */ private boolean isIMAllowed() { // check are we allowed to change back to im mode Object descr = chatPanel.getChatSession().getDescriptor(); if (descr instanceof MetaContact) { List<Contact> imContact = ((MetaContact) descr) .getContactsForOperationSet(OperationSetBasicInstantMessaging.class); if (imContact == null || imContact.size() == 0) return false; } else if (descr instanceof SourceContact) { List<ContactDetail> imContact = ((SourceContact) descr) .getContactDetails(OperationSetBasicInstantMessaging.class); if (imContact == null || imContact.size() == 0) return false; } return true; } /** * The <tt>SendMessageAction</tt> is an <tt>AbstractAction</tt> that * sends the text that is currently in the write message area. */ private class SendMessageAction extends AbstractAction { public void actionPerformed(ActionEvent e) { // chatPanel.stopTypingNotifications(); chatPanel.sendButtonDoClick(); } } /** * The <tt>NewLineAction</tt> is an <tt>AbstractAction</tt> that types * an enter in the write message area. */ private class NewLineAction extends AbstractAction { public void actionPerformed(ActionEvent e) { int caretPosition = editorPane.getCaretPosition(); HTMLDocument doc = (HTMLDocument) editorPane.getDocument(); try { doc.insertString(caretPosition, "\n", null); } catch (BadLocationException e1) { logger.error("Could not insert <br> to the document.", e1); } editorPane.setCaretPosition(caretPosition + 1); } } /** * Handles the <tt>UndoableEditEvent</tt>, by adding the content edit to * the <tt>UndoManager</tt>. * * @param e The <tt>UndoableEditEvent</tt>. */ public void undoableEditHappened(UndoableEditEvent e) { this.undo.addEdit(e.getEdit()); } /** * Implements the undo operation. */ private void undo() { try { undo.undo(); } catch (CannotUndoException e) { logger.error("Unable to undo.", e); } } /** * Implements the redo operation. */ private void redo() { try { undo.redo(); } catch (CannotRedoException e) { logger.error("Unable to redo.", e); } } /** * Sends typing notifications when user types. * * @param e the event. */ public void keyTyped(KeyEvent e) { if (ConfigurationUtils.isSendTypingNotifications() && !smsMode) { if (typingState != OperationSetTypingNotifications.STATE_TYPING) { stoppedTypingTimer.setDelay(2 * 1000); typingState = OperationSetTypingNotifications.STATE_TYPING; int result = chatPanel.getChatSession().getCurrentChatTransport() .sendTypingNotification(typingState); if (result == ChatPanel.TYPING_NOTIFICATION_SUCCESSFULLY_SENT) typingTimer.start(); } if (!stoppedTypingTimer.isRunning()) stoppedTypingTimer.start(); else stoppedTypingTimer.restart(); } } /** * When CTRL+Z is pressed invokes the <code>ChatWritePanel.undo()</code> * method, when CTRL+R is pressed invokes the * <code>ChatWritePanel.redo()</code> method. * * @param e the <tt>KeyEvent</tt> that notified us */ public void keyPressed(KeyEvent e) { if ((e.getModifiers() & KeyEvent.CTRL_MASK) == KeyEvent.CTRL_MASK && (e.getKeyCode() == KeyEvent.VK_Z) // And not ALT(right ALT gives CTRL + ALT). && (e.getModifiers() & KeyEvent.ALT_MASK) != KeyEvent.ALT_MASK) { if (undo.canUndo()) undo(); } else if ((e.getModifiers() & KeyEvent.CTRL_MASK) == KeyEvent.CTRL_MASK && (e.getKeyCode() == KeyEvent.VK_R) // And not ALT(right ALT gives CTRL + ALT). && (e.getModifiers() & KeyEvent.ALT_MASK) != KeyEvent.ALT_MASK) { if (undo.canRedo()) redo(); } else if (e.getKeyCode() == KeyEvent.VK_TAB) { if (!(chatPanel.getChatSession() instanceof ConferenceChatSession)) return; e.consume(); int index = ((JEditorPane) e.getSource()).getCaretPosition(); StringBuffer message = new StringBuffer(chatPanel.getMessage()); int position = index - 1; while (position > 0 && (message.charAt(position) != ' ')) { position--; } if (position != 0) position++; String sequence = message.substring(position, index); if (sequence.length() <= 0) { // Do not look for matching contacts if the matching pattern is // 0 chars long, since all contacts will match. return; } Iterator<ChatContact<?>> iter = chatPanel.getChatSession().getParticipants(); ArrayList<String> contacts = new ArrayList<String>(); while (iter.hasNext()) { ChatContact<?> c = iter.next(); if (c.getName().length() >= (index - position) && c.getName().substring(0, index - position).equals(sequence)) { message.replace(position, index, c.getName().substring(0, index - position)); contacts.add(c.getName()); } } if (contacts.size() > 1) { char key = contacts.get(0).charAt(index - position - 1); int pos = index - position - 1; boolean flag = true; while (flag) { try { for (String name : contacts) { if (key != name.charAt(pos)) { flag = false; } } if (flag) { pos++; key = contacts.get(0).charAt(pos); } } catch (IndexOutOfBoundsException exp) { flag = false; } } message.replace(position, index, contacts.get(0).substring(0, pos)); Iterator<String> contactIter = contacts.iterator(); String contactList = "<DIV align='left'><h5>"; while (contactIter.hasNext()) { contactList += contactIter.next() + " "; } contactList += "</h5></DIV>"; chatPanel.getChatConversationPanel().appendMessageToEnd(contactList, ChatHtmlUtils.HTML_CONTENT_TYPE); } else if (contacts.size() == 1) { String limiter = (position == 0) ? ": " : ""; message.replace(position, index, contacts.get(0) + limiter); } try { ((JEditorPane) e.getSource()).getDocument().remove(0, ((JEditorPane) e.getSource()).getDocument().getLength()); ((JEditorPane) e.getSource()).getDocument().insertString(0, message.toString(), null); } catch (BadLocationException ex) { ex.printStackTrace(); } } else if (e.getKeyCode() == KeyEvent.VK_UP) { // Only enters editing mode if the write panel is empty in // order not to lose the current message contents, if any. if (this.chatPanel.getLastSentMessageUID() != null && this.chatPanel.isWriteAreaEmpty()) { this.chatPanel.startLastMessageCorrection(); e.consume(); } } else if (e.getKeyCode() == KeyEvent.VK_DOWN) { if (chatPanel.isMessageCorrectionActive()) { Document doc = editorPane.getDocument(); if (editorPane.getCaretPosition() == doc.getLength()) { chatPanel.stopMessageCorrection(); } } } } public void keyReleased(KeyEvent e) { } /** * Performs actions when typing timer has expired. * * @param e the <tt>ActionEvent</tt> that notified us */ public void actionPerformed(ActionEvent e) { Object source = e.getSource(); if (typingTimer.equals(source)) { if (typingState == OperationSetTypingNotifications.STATE_TYPING) { chatPanel.getChatSession().getCurrentChatTransport() .sendTypingNotification(OperationSetTypingNotifications.STATE_TYPING); } } else if (stoppedTypingTimer.equals(source)) { typingTimer.stop(); if (typingState == OperationSetTypingNotifications.STATE_TYPING) { try { typingState = OperationSetTypingNotifications.STATE_PAUSED; int result = chatPanel.getChatSession().getCurrentChatTransport() .sendTypingNotification(typingState); if (result == ChatPanel.TYPING_NOTIFICATION_SUCCESSFULLY_SENT) stoppedTypingTimer.setDelay(3 * 1000); } catch (Exception ex) { logger.error("Failed to send typing notifications.", ex); } } else if (typingState == OperationSetTypingNotifications.STATE_PAUSED) { stopTypingTimer(); } } } /** * Stops the timer and sends a notification message. */ public void stopTypingTimer() { typingState = OperationSetTypingNotifications.STATE_STOPPED; int result = chatPanel.getChatSession().getCurrentChatTransport().sendTypingNotification(typingState); if (result == ChatPanel.TYPING_NOTIFICATION_SUCCESSFULLY_SENT) stoppedTypingTimer.stop(); } /** * Opens the <tt>WritePanelRightButtonMenu</tt> when user clicks with the * right mouse button on the editor area. * * @param e the <tt>MouseEvent</tt> that notified us */ public void mouseClicked(MouseEvent e) { if ((e.getModifiers() & InputEvent.BUTTON3_MASK) != 0 || (e.isControlDown() && !e.isMetaDown())) { Point p = e.getPoint(); SwingUtilities.convertPointToScreen(p, e.getComponent()); //SPELLCHECK ArrayList<JMenuItem> contributedMenuEntries = new ArrayList<JMenuItem>(); for (ChatMenuListener listener : this.menuListeners) { contributedMenuEntries.addAll(listener.getMenuElements(this.chatPanel, e)); } for (JMenuItem item : contributedMenuEntries) { rightButtonMenu.add(item); } JPopupMenu rightMenu = rightButtonMenu.makeMenu(contributedMenuEntries); rightMenu.setInvoker(editorPane); rightMenu.setLocation(p.x, p.y); rightMenu.setVisible(true); } } public void mousePressed(MouseEvent e) { } public void mouseReleased(MouseEvent e) { } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } /** * Returns the <tt>WritePanelRightButtonMenu</tt> opened in this panel. * Used by the <tt>ChatWindow</tt>, when the ESC button is pressed, to * check if there is an open menu, which should be closed. * * @return the <tt>WritePanelRightButtonMenu</tt> opened in this panel */ public WritePanelRightButtonMenu getRightButtonMenu() { return rightButtonMenu; } /** * Returns the write area text as an html text. * * @return the write area text as an html text. */ public String getTextAsHtml() { String msgText = editorPane.getText(); String formattedString = msgText.replaceAll("<html>|<head>|<body>|</html>|</head>|</body>", ""); formattedString = extractFormattedText(formattedString); // Returned string is formatted with newlines, etc. so we need to get // rid of them before checking for the ending <br/>. formattedString = formattedString.trim(); if (formattedString.endsWith("<BR/>")) formattedString = formattedString.substring(0, formattedString.lastIndexOf("<BR/>")); return formattedString; } /** * Returns the write area text as a plain text without any formatting. * * @return the write area text as a plain text without any formatting. */ public String getText() { try { Document doc = editorPane.getDocument(); return doc.getText(0, doc.getLength()); } catch (BadLocationException e) { logger.error("Could not obtain write area text.", e); } return null; } /** * Clears write message area. */ public void clearWriteArea() { try { this.editorPane.getDocument().remove(0, editorPane.getDocument().getLength()); if (smsMode) { // use this to reset sms counter setSmsLabelVisible(true); // set the reset values smsCharCountLabel.setText(String.valueOf(smsCharCount)); smsNumberLabel.setText(String.valueOf(smsNumberCount)); } } catch (BadLocationException e) { logger.error("Failed to obtain write panel document content.", e); } } /** * Appends the given text to the end of the contained HTML document. This * method is used to insert smileys when user selects a smiley from the * menu. * * @param text the text to append. */ public void appendText(String text) { HTMLDocument doc = (HTMLDocument) editorPane.getDocument(); Element currentElement = doc.getCharacterElement(editorPane.getCaretPosition()); try { doc.insertAfterEnd(currentElement, text); } catch (BadLocationException e) { logger.error("Insert in the HTMLDocument failed.", e); } catch (IOException e) { logger.error("Insert in the HTMLDocument failed.", e); } this.editorPane.setCaretPosition(doc.getLength()); } /** * Return all html paragraph content separated by <BR/> tags. * * @param msgText the html text. * @return the string containing only paragraph content. */ private String extractFormattedText(String msgText) { String resultString = msgText.replaceAll("<p\\b[^>]*>", ""); return resultString.replaceAll("<\\/p>", "<BR/>"); } /** * Initializes the send via label and selector box. * * @return the chat transport selector box */ private Component createChatTransportSelectorBox() { // Initialize the "send via" selector box and adds it to the send panel. if (transportSelectorBox == null) { transportSelectorBox = new ChatTransportSelectorBox(chatPanel, chatPanel.getChatSession(), chatPanel.getChatSession().getCurrentChatTransport()); if ((ConfigurationUtils.isHideAccountSelectionWhenPossibleEnabled() && transportSelectorBox.getMenu().getItemCount() <= 1) || !isIMAllowed()) transportSelectorBox.setVisible(false); } return transportSelectorBox; } /** * * @param isVisible */ public void setTransportSelectorBoxVisible(boolean isVisible) { if (isVisible) { if (transportSelectorBox == null) { createChatTransportSelectorBox(); if (!transportSelectorBox.getMenu().isEnabled()) { // Show a message to the user that IM is not possible. chatPanel.getChatConversationPanel() .appendMessageToEnd( "<h5>" + StringEscapeUtils.escapeHtml4(GuiActivator.getResources() .getI18NString("service.gui.MSG_NOT_POSSIBLE")) + "</h5>", ChatHtmlUtils.HTML_CONTENT_TYPE); } else { GridBagConstraints constraints = new GridBagConstraints(); constraints.anchor = GridBagConstraints.NORTHEAST; constraints.fill = GridBagConstraints.NONE; constraints.gridx = 0; constraints.gridy = 0; constraints.weightx = 0f; constraints.weighty = 0f; constraints.gridheight = 1; constraints.gridwidth = 1; centerPanel.add(transportSelectorBox, constraints, 0); } } else { if (ConfigurationUtils.isHideAccountSelectionWhenPossibleEnabled() && transportSelectorBox.getMenu().getItemCount() <= 1) { transportSelectorBox.setVisible(false); } { transportSelectorBox.setVisible(true); } centerPanel.repaint(); } } else if (transportSelectorBox != null) { transportSelectorBox.setVisible(false); centerPanel.repaint(); } } /** * Selects the given chat transport in the send via box. * * @param chatTransport The chat transport to be selected. * @param isMessageOrFileTransferReceived Boolean telling us if this change * of the chat transport correspond to an effective switch to this new * transform (a mesaage received from this transport, or a file transfer * request received, or if the resource timeouted), or just a status update * telling us a new chatTransport is now available (i.e. another device has * startup). */ public void setSelectedChatTransport(final ChatTransport chatTransport, final boolean isMessageOrFileTransferReceived) { // We need to be sure that the following code is executed in the event // dispatch thread. if (!SwingUtilities.isEventDispatchThread()) { SwingUtilities.invokeLater(new Runnable() { public void run() { setSelectedChatTransport(chatTransport, isMessageOrFileTransferReceived); } }); return; } // Check if this contact provider can manages several resources and thus // provides a resource timeout via the basic IM operation set. long timeout = -1; OperationSetBasicInstantMessaging opSetBasicIM = chatTransport.getProtocolProvider() .getOperationSet(OperationSetBasicInstantMessaging.class); if (opSetBasicIM != null) { timeout = opSetBasicIM.getInactivityTimeout(); } if (isMessageOrFileTransferReceived) { isOutdatedResource = false; } // If this contact supports several resources, then schedule the timer: // - If the resource is outdated, then trigger the timer now (to try to // switch to the bare ID if now available). // - If the new reousrce transport is really effective (i.e. we have // received a message from this resource). if (timeout != -1 && (isMessageOrFileTransferReceived || isOutdatedResource)) { // If there was already a timeout, but the bare ID was not available // (i.e. a single resource present). Then call the timeout procedure // now in order to switch to the bare ID. if (isOutdatedResource) { timeout = 0; } // Cancels the preceding timer. if (outdatedResourceTimer != null) { outdatedResourceTimer.cancel(); outdatedResourceTimer.purge(); } // Schedules the timer. if (chatTransport.getResourceName() != null) { OutdatedResourceTimerTask task = new OutdatedResourceTimerTask(); outdatedResourceTimer = new java.util.Timer(); outdatedResourceTimer.schedule(task, timeout); } } // Sets the new resource transport is really effective (i.e. we have // received a message from this resource). // if we do not have any selected resource, or the currently selected // if offline if (transportSelectorBox != null && (isMessageOrFileTransferReceived || (!transportSelectorBox.hasSelectedTransport() || !chatPanel.getChatSession().getCurrentChatTransport().getStatus().isOnline()))) { transportSelectorBox.setSelected(chatTransport); } } /** * Adds the given chatTransport to the given send via selector box. * * @param chatTransport the transport to add */ public void addChatTransport(final ChatTransport chatTransport) { // We need to be sure that the following code is executed in the event // dispatch thread. if (!SwingUtilities.isEventDispatchThread()) { SwingUtilities.invokeLater(new Runnable() { public void run() { addChatTransport(chatTransport); } }); return; } if (transportSelectorBox != null) { transportSelectorBox.addChatTransport(chatTransport); // it was hidden cause we wanted to hide when there is only one // provider if (!transportSelectorBox.isVisible() && ConfigurationUtils.isHideAccountSelectionWhenPossibleEnabled() && transportSelectorBox.getMenu().getItemCount() > 1) { transportSelectorBox.setVisible(true); } } } /** * Updates the status of the given chat transport in the send via selector * box and notifies the user for the status change. * @param chatTransport the <tt>chatTransport</tt> to update */ public void updateChatTransportStatus(final ChatTransport chatTransport) { // We need to be sure that the following code is executed in the event // dispatch thread. if (!SwingUtilities.isEventDispatchThread()) { SwingUtilities.invokeLater(new Runnable() { public void run() { updateChatTransportStatus(chatTransport); } }); return; } if (transportSelectorBox != null) transportSelectorBox.updateTransportStatus(chatTransport); } /** * Opens the selector box containing the protocol contact icons. * This is the menu, where user could select the protocol specific * contact to communicate through. */ public void openChatTransportSelectorBox() { transportSelectorBox.getMenu().doClick(); } /** * Removes the given chat status state from the send via selector box. * * @param chatTransport the transport to remove */ public void removeChatTransport(final ChatTransport chatTransport) { // We need to be sure that the following code is executed in the event // dispatch thread. if (!SwingUtilities.isEventDispatchThread()) { SwingUtilities.invokeLater(new Runnable() { public void run() { removeChatTransport(chatTransport); } }); return; } if (transportSelectorBox != null) transportSelectorBox.removeChatTransport(chatTransport); if (transportSelectorBox != null && transportSelectorBox.getMenu().getItemCount() == 1 && ConfigurationUtils.isHideAccountSelectionWhenPossibleEnabled()) { transportSelectorBox.setVisible(false); } } /** * Show the sms menu. * @param isVisible <tt>true</tt> to show the sms menu, <tt>false</tt> - * otherwise */ public void setSmsLabelVisible(boolean isVisible) { if (disableMergedSmsMode && isIMAllowed()) return; // Re-init sms count properties. smsCharCount = 160; smsNumberCount = 1; smsButton.setVisible(isVisible); centerPanel.repaint(); } /** * Saves the given font configuration as default, thus making it the default * configuration for all chats. * * @param fontFamily the font family * @param fontSize the font size * @param isBold indicates if the font is bold * @param isItalic indicates if the font is italic * @param isUnderline indicates if the font is underline */ public void saveDefaultFontConfiguration(String fontFamily, int fontSize, boolean isBold, boolean isItalic, boolean isUnderline, Color color) { ConfigurationUtils.setChatDefaultFontFamily(fontFamily); ConfigurationUtils.setChatDefaultFontSize(fontSize); ConfigurationUtils.setChatFontIsBold(isBold); ConfigurationUtils.setChatFontIsItalic(isItalic); ConfigurationUtils.setChatFontIsUnderline(isUnderline); ConfigurationUtils.setChatDefaultFontColor(color); } /** * Sets the font family and size * @param family the family name * @param size the size */ public void setFontFamilyAndSize(String family, int size) { // Family ActionEvent evt = new ActionEvent(editorPane, ActionEvent.ACTION_PERFORMED, family); Action action = new StyledEditorKit.FontFamilyAction(family, family); action.actionPerformed(evt); // Size evt = new ActionEvent(editorPane, ActionEvent.ACTION_PERFORMED, Integer.toString(size)); action = new StyledEditorKit.FontSizeAction(Integer.toString(size), size); action.actionPerformed(evt); } /** * Enables the bold style * @param b TRUE enable - FALSE disable */ public void setBoldStyleEnable(boolean b) { StyledEditorKit editorKit = (StyledEditorKit) editorPane.getEditorKit(); MutableAttributeSet attr = editorKit.getInputAttributes(); if (b && !StyleConstants.isBold(attr)) { setStyleConstant(new HTMLEditorKit.BoldAction(), StyleConstants.Bold); } } /** * Enables the italic style * @param b TRUE enable - FALSE disable */ public void setItalicStyleEnable(boolean b) { StyledEditorKit editorKit = (StyledEditorKit) editorPane.getEditorKit(); MutableAttributeSet attr = editorKit.getInputAttributes(); if (b && !StyleConstants.isItalic(attr)) { setStyleConstant(new HTMLEditorKit.ItalicAction(), StyleConstants.Italic); } } /** * Enables the underline style * @param b TRUE enable - FALSE disable */ public void setUnderlineStyleEnable(boolean b) { StyledEditorKit editorKit = (StyledEditorKit) editorPane.getEditorKit(); MutableAttributeSet attr = editorKit.getInputAttributes(); if (b && !StyleConstants.isUnderline(attr)) { setStyleConstant(new HTMLEditorKit.UnderlineAction(), StyleConstants.Underline); } } /** * Sets the font color * @param color the color */ public void setFontColor(Color color) { ActionEvent evt = new ActionEvent(editorPane, ActionEvent.ACTION_PERFORMED, ""); Action action = new HTMLEditorKit.ForegroundAction(Integer.toString(color.getRGB()), color); action.actionPerformed(evt); } /** * Sets the given style constant. * * @param action the action * @param styleConstant the style constant */ private void setStyleConstant(Action action, Object styleConstant) { ActionEvent event = new ActionEvent(editorPane, ActionEvent.ACTION_PERFORMED, styleConstant.toString()); action.actionPerformed(event); } /** * Adds the given {@link ChatMenuListener} to this <tt>Chat</tt>. * The <tt>ChatMenuListener</tt> is used to determine menu elements * that should be added on right clicks. * * @param l the <tt>ChatMenuListener</tt> to add */ public void addChatEditorMenuListener(ChatMenuListener l) { this.menuListeners.add(l); } /** * Removes the given {@link ChatMenuListener} to this <tt>Chat</tt>. * The <tt>ChatMenuListener</tt> is used to determine menu elements * that should be added on right clicks. * * @param l the <tt>ChatMenuListener</tt> to add */ public void removeChatEditorMenuListener(ChatMenuListener l) { this.menuListeners.remove(l); } /** * Reloads menu. */ public void loadSkin() { getRightButtonMenu().loadSkin(); } public void changedUpdate(DocumentEvent documentevent) { } /** * Updates write panel size and adjusts sms properties if the sms menu * is visible. * * @param event the <tt>DocumentEvent</tt> that notified us */ public void insertUpdate(DocumentEvent event) { // If we're in sms mode count the chars typed. if (smsButton.isVisible()) { updateSmsCounters(event.getDocument().getLength()); } } /** * Updates write panel size and adjusts sms properties if the sms menu * is visible. * * @param event the <tt>DocumentEvent</tt> that notified us */ public void removeUpdate(DocumentEvent event) { // If we're in sms mode count the chars typed. if (smsButton.isVisible()) { updateSmsCounters(event.getDocument().getLength()); } } /** * Updates sms counters, 160 chars in one sms. * @param documentLength the current document length */ private void updateSmsCounters(int documentLength) { smsCharCount = 160 - documentLength % 160; smsNumberCount = 1 + documentLength / 160; smsCharCountLabel.setText(String.valueOf(smsCharCount)); smsNumberLabel.setText(String.valueOf(smsNumberCount)); } /** * Sets the background of the write area to the specified color. * * @param color The color to set the background to. */ public void setEditorPaneBackground(Color color) { this.centerPanel.setBackground(color); this.editorPane.setBackground(color); } /** * The task called when the current transport resource timed-out (no * acitivty since a long time). Then this task resets the destination to the * bare id. */ private class OutdatedResourceTimerTask extends TimerTask { /** * The action to be performed by this timer task. */ public void run() { outdatedResourceTimer = null; if (chatPanel.getChatSession() != null) { Iterator<ChatTransport> transports = chatPanel.getChatSession().getChatTransports(); ChatTransport transport = null; while (transports.hasNext()) { transport = transports.next(); // We found the bare ID, then set it as the current resource // transport. // choose only online resources if (transport.getResourceName() == null && transport.getStatus().isOnline()) { isOutdatedResource = false; setSelectedChatTransport(transport, true); return; } } } // If there is no bare ID available, then set the current resource // transport as outdated. isOutdatedResource = true; } } /** * Initializes plug-in components for this container. */ void initPluginComponents() { // Search for plugin components registered through the OSGI bundle // context. Collection<ServiceReference<PluginComponentFactory>> serRefs; String osgiFilter = "(" + net.java.sip.communicator.service.gui.Container.CONTAINER_ID + "=" + net.java.sip.communicator.service.gui.Container.CONTAINER_CHAT_WRITE_PANEL.getID() + ")"; try { serRefs = GuiActivator.bundleContext.getServiceReferences(PluginComponentFactory.class, osgiFilter); } catch (InvalidSyntaxException ex) { serRefs = null; logger.error("Could not obtain plugin reference.", ex); } if ((serRefs != null) && !serRefs.isEmpty()) { for (ServiceReference<PluginComponentFactory> serRef : serRefs) { PluginComponentFactory factory = GuiActivator.bundleContext.getService(serRef); PluginComponent component = factory.getPluginComponentInstance(this); this.pluginComponents.add(component); ChatSession chatSession = chatPanel.getChatSession(); if (chatSession != null) { ChatTransport currentTransport = chatSession.getCurrentChatTransport(); Object currentDescriptor = currentTransport.getDescriptor(); if (currentDescriptor instanceof Contact) { Contact contact = (Contact) currentDescriptor; component.setCurrentContact(contact, currentTransport.getResourceName()); } } Object c = component.getComponent(); if (c == null) continue; GridBagConstraints constraints = new GridBagConstraints(); constraints.anchor = GridBagConstraints.NORTHEAST; constraints.fill = GridBagConstraints.NONE; constraints.gridy = 0; constraints.gridheight = 1; constraints.weightx = 0f; constraints.weighty = 0f; constraints.insets = new Insets(0, 3, 0, 0); centerPanel.add((Component) c, constraints); } } GuiActivator.getUIService().addPluginComponentListener(this); this.centerPanel.repaint(); } /** * Indicates that a new plugin component has been added. Adds it to this * container if it belongs to it. * * @param event the <tt>PluginComponentEvent</tt> that notified us */ public void pluginComponentAdded(PluginComponentEvent event) { PluginComponentFactory factory = event.getPluginComponentFactory(); if (!factory.getContainer() .equals(net.java.sip.communicator.service.gui.Container.CONTAINER_CHAT_WRITE_PANEL)) return; PluginComponent component = factory.getPluginComponentInstance(this); this.pluginComponents.add(component); ChatSession chatSession = chatPanel.getChatSession(); if (chatSession != null) { ChatTransport currentTransport = chatSession.getCurrentChatTransport(); Object currentDescriptor = currentTransport.getDescriptor(); if (currentDescriptor instanceof Contact) { Contact contact = (Contact) currentDescriptor; component.setCurrentContact(contact, currentTransport.getResourceName()); } } GridBagConstraints constraints = new GridBagConstraints(); constraints.anchor = GridBagConstraints.NORTHEAST; constraints.fill = GridBagConstraints.NONE; constraints.gridy = 0; constraints.gridheight = 1; constraints.weightx = 0f; constraints.weighty = 0f; constraints.insets = new Insets(0, 3, 0, 0); centerPanel.add((Component) component.getComponent(), constraints); this.centerPanel.repaint(); } /** * Removes the according plug-in component from this container. * * @param event the <tt>PluginComponentEvent</tt> that notified us */ public void pluginComponentRemoved(PluginComponentEvent event) { PluginComponentFactory factory = event.getPluginComponentFactory(); if (!factory.getContainer() .equals(net.java.sip.communicator.service.gui.Container.CONTAINER_CHAT_WRITE_PANEL)) return; Component c = (Component) factory.getPluginComponentInstance(this).getComponent(); this.pluginComponents.remove(c); this.centerPanel.remove(c); this.centerPanel.repaint(); } /** * Event in case of chat transport changed, for example because a different * transport was selected. * * @param chatSession the chat session */ @Override public void currentChatTransportChanged(ChatSession chatSession) { List<PluginComponent> components; synchronized (this.pluginComponents) { components = new ArrayList<PluginComponent>(this.pluginComponents); } // determine contact instance to use in event handling when calling // setCurrentContact final Contact contact; final Object descriptor = chatSession.getDescriptor(); if (descriptor instanceof MetaContact) { contact = ((MetaContact) descriptor).getDefaultContact(); } else if (descriptor instanceof Contact) { contact = (Contact) descriptor; } else if (descriptor == null) { // In case of null contact, just call setCurrentContact for // null Contact and get out. Nothing else to do here. for (PluginComponent c : components) { c.setCurrentContact((Contact) null); } return; } else { logger.warn(String.format("Unsupported descriptor type %s (%s)," + "this event will not be propagated.", descriptor, descriptor.getClass().getCanonicalName())); return; } // Call setCurrentContact on all registered pluginComponents such that // all get updated on the new state of the chat session final String resourceName = chatSession.getCurrentChatTransport().getResourceName(); for (PluginComponent c : components) { try { c.setCurrentContact(contact, resourceName); } catch (RuntimeException e) { logger.error("BUG: setCurrentContact of PluginComponent instance: " + c.getClass().getCanonicalName() + " throws a RuntimeException.", e); } } } @Override public void currentChatTransportUpdated(int eventID) { // Nothing to do here, since we do not need to communicate update events } }