/* * Copyright (C) 2004-2011 Jive Software. All rights reserved. * * 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 org.jivesoftware.spark.ui; import org.jivesoftware.resource.Res; import org.jivesoftware.resource.SparkRes; import org.jivesoftware.smack.ConnectionListener; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.StanzaListener; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.roster.Roster; import org.jivesoftware.smack.roster.RosterEntry; import org.jivesoftware.smackx.chatstates.ChatState; import org.jivesoftware.smackx.jiveproperties.packet.JivePropertiesExtension; import org.jivesoftware.spark.ChatAreaSendField; import org.jivesoftware.spark.SparkManager; import org.jivesoftware.spark.component.BackgroundPanel; import org.jivesoftware.spark.component.RolloverButton; import org.jivesoftware.spark.plugin.ContextMenuListener; import org.jivesoftware.spark.ui.rooms.GroupChatRoom; import org.jivesoftware.spark.util.GraphicUtils; import org.jivesoftware.spark.util.SwingWorker; import org.jivesoftware.spark.util.TaskEngine; import org.jivesoftware.spark.util.UIComponentRegistry; import org.jivesoftware.spark.util.log.Log; import org.jivesoftware.sparkimpl.settings.local.LocalPreferences; import org.jivesoftware.sparkimpl.settings.local.SettingsManager; import org.jxmpp.jid.EntityBareJid; import org.jxmpp.jid.EntityFullJid; import org.jxmpp.jid.parts.Resourcepart; import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import java.awt.*; import java.awt.event.*; import java.io.File; import java.util.*; import java.util.List; /** * The base implementation of all ChatRoom conversations. You would implement this class to have most types of Chat. */ public abstract class ChatRoom extends BackgroundPanel implements ActionListener, StanzaListener, DocumentListener, ConnectionListener, FocusListener, ContextMenuListener, ChatFrameToFrontListener { private final JPanel chatPanel; private final JSplitPane splitPane; private JSplitPane verticalSplit; private final JLabel notificationLabel; private final TranscriptWindow transcriptWindow; private final ChatAreaSendField chatAreaButton; private final ChatToolBar toolbar; private final JScrollPane textScroller; private final JPanel bottomPanel; private final JPanel editorWrapperBar; private final JPanel editorBarRight; private final JPanel editorBarLeft; private JPanel chatWindowPanel; private int unreadMessageCount; private boolean mousePressed; private List<ChatRoomClosingListener> closingListeners = new ArrayList<>(); private ChatRoomTransferHandler transferHandler; private final List<String> packetIDList; private final List<MessageListener> messageListeners; private List<Message> transcript; private List<FileDropListener> fileDropListeners; private MouseAdapter transcriptWindowMouseListener; private KeyAdapter chatEditorKeyListener; private ChatFrame _chatFrame; private RolloverButton _alwaysOnTopItem; private boolean _isAlwaysOnTopActive; // Chat state private TimerTask typingTimerTask; private long lastNotificationSentTime; private ChatState lastNotificationSent; private long pauseTimePeriod = 2000; private long inactiveTimePeriod = 120000; /** * Initializes the base layout and base background color. */ protected ChatRoom() { chatPanel = new JPanel(new GridBagLayout()); transcriptWindow = UIComponentRegistry.createTranscriptWindow(); splitPane = new JSplitPane(); packetIDList = new ArrayList<>(); notificationLabel = new JLabel(); toolbar = new ChatToolBar(); bottomPanel = new JPanel(); messageListeners = new ArrayList<>(); transcript = new ArrayList<>(); editorWrapperBar = new JPanel(new BorderLayout()); editorBarLeft = new JPanel(new FlowLayout(FlowLayout.LEFT, 1, 1)); editorBarRight = new JPanel(new FlowLayout(FlowLayout.RIGHT, 1, 1)); editorWrapperBar.add(editorBarLeft, BorderLayout.WEST); editorWrapperBar.add(editorBarRight, BorderLayout.EAST); fileDropListeners = new ArrayList<>(); transcriptWindowMouseListener = new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if(e.getClickCount()!=2){ getChatInputEditor().requestFocus(); } } @Override public void mouseReleased(MouseEvent e) { mousePressed = false; if (transcriptWindow.getSelectedText() == null) { getChatInputEditor().requestFocus(); } } @Override public void mousePressed(MouseEvent e) { mousePressed = true; } }; transcriptWindow.addMouseListener(transcriptWindowMouseListener); chatAreaButton = new ChatAreaSendField(Res.getString("button.send")); textScroller = new JScrollPane(transcriptWindow); textScroller.setBackground(transcriptWindow.getBackground()); textScroller.getViewport().setBackground(Color.white); transcriptWindow.setBackground(Color.white); getChatInputEditor().setSelectedTextColor((Color)UIManager.get("ChatInput.SelectedTextColor")); getChatInputEditor().setSelectionColor((Color)UIManager.get("ChatInput.SelectionColor")); setLayout(new GridBagLayout()); // Remove Default Beveled Borders splitPane.setBorder(null); splitPane.setOneTouchExpandable(false); // Add Vertical Split Pane verticalSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT); add(verticalSplit, new GridBagConstraints(0, 1, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); verticalSplit.setBorder(null); verticalSplit.setOneTouchExpandable(false); verticalSplit.setTopComponent(splitPane); textScroller.setAutoscrolls(true); // For the first 5*150ms we wait for transcript to load and move // scrollpane to max postion if size of scrollpane changed textScroller.getVerticalScrollBar().addAdjustmentListener(new AdjustmentListener() { private boolean scrollAtStart = false; @Override public void adjustmentValueChanged(AdjustmentEvent e) { if (!scrollAtStart) { scrollAtStart = true; SwingWorker thread = new SwingWorker() { @Override public Object construct() { int start = textScroller.getVerticalScrollBar().getMaximum(); int second; int i = 0; do { try { Thread.sleep(150); second = textScroller.getVerticalScrollBar().getMaximum(); if (start == second) { ++i; } else { scrollToBottom(); getTranscriptWindow().repaint(); } start = second; } catch (InterruptedException e) { e.printStackTrace(); } } while (i < 5); return null; } }; thread.start(); } } }); // Speed up scrolling. It was way too slow. textScroller.getVerticalScrollBar().setBlockIncrement(200); textScroller.getVerticalScrollBar().setUnitIncrement(20); chatWindowPanel = new JPanel(); chatWindowPanel.setLayout(new GridBagLayout()); chatWindowPanel.add(textScroller, new GridBagConstraints(0, 10, 1, 1, 1.0, 1.0, GridBagConstraints.WEST, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); chatWindowPanel.setOpaque(false); // Layout Components chatPanel.add(chatWindowPanel, new GridBagConstraints(0, 1, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, getChatPanelInsets(), 0, 0)); // Add Chat Panel to Split Pane splitPane.setLeftComponent(chatPanel); // Add edit buttons to Chat Room editorBarLeft.setOpaque(false); chatPanel.setOpaque(false); bottomPanel.setOpaque(false); splitPane.setOpaque(false); bottomPanel.setLayout(new GridBagLayout()); bottomPanel.add(chatAreaButton, new GridBagConstraints(0, 1, 5, 1, 1.0, 1.0, GridBagConstraints.WEST, GridBagConstraints.BOTH, getChatAreaInsets(), 0, 35)); bottomPanel.add(editorWrapperBar, new GridBagConstraints(0, 0, 5, 1, 0.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, getEditorWrapperInsets(), 0, 0)); // Set bottom panel border bottomPanel.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, new Color(197, 213, 230))); verticalSplit.setOpaque(false); verticalSplit.setBottomComponent(bottomPanel); verticalSplit.setResizeWeight(1.0); verticalSplit.setDividerSize(2); // Add listener to send button chatAreaButton.getButton().addActionListener(this); // Add Key Listener to Send Field getChatInputEditor().getDocument().addDocumentListener(this); // Add Key Listener to Send Field chatEditorKeyListener = new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { checkForEnter(e); } }; getChatInputEditor().addKeyListener(chatEditorKeyListener); getChatInputEditor().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("ctrl F4"), "closeTheRoom"); getChatInputEditor().getActionMap().put("closeTheRoom", new AbstractAction("closeTheRoom") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent evt) { // Leave this chat. closeChatRoom(); } }); getChatInputEditor().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("ctrl SPACE"), "handleCompletion"); getChatInputEditor().getActionMap().put("handleCompletion", new AbstractAction("handleCompletion") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent evt) { // handle name completion. try { handleNickNameCompletion(); } catch (ChatRoomNotFoundException e) { Log.error("ctlr-space nickname find", e); } } }); _isAlwaysOnTopActive = SettingsManager.getLocalPreferences().isChatWindowAlwaysOnTop(); _alwaysOnTopItem = UIComponentRegistry.getButtonFactory().createAlwaysOnTop(_isAlwaysOnTopActive); _alwaysOnTopItem.addActionListener( actionEvent -> { if (!_isAlwaysOnTopActive) { SettingsManager.getLocalPreferences().setChatWindowAlwaysOnTop(true); _chatFrame.setWindowAlwaysOnTop(true); _isAlwaysOnTopActive = true; _alwaysOnTopItem.setIcon(SparkRes.getImageIcon("FRAME_ALWAYS_ON_TOP_ACTIVE")); } else { SettingsManager.getLocalPreferences().setChatWindowAlwaysOnTop(false); _chatFrame.setWindowAlwaysOnTop(false); _isAlwaysOnTopActive = false; _alwaysOnTopItem.setIcon(SparkRes.getImageIcon("FRAME_ALWAYS_ON_TOP_DEACTIVE")); } } ); editorBarRight.add(_alwaysOnTopItem); // Initially, set the right pane to null to keep it empty. getSplitPane().setRightComponent(null); notificationLabel.setIcon(SparkRes.getImageIcon(SparkRes.BLANK_IMAGE)); getTranscriptWindow().addContextMenuListener(this); transferHandler = new ChatRoomTransferHandler(this); getTranscriptWindow().setTransferHandler(transferHandler); getChatInputEditor().setTransferHandler(transferHandler); addToolbar(); // Add Connection Listener SparkManager.getConnection().addConnectionListener(this); // Add Focus Listener addFocusListener(this); setChatState( ChatState.active ); createChatStateTimerTask(); scrollToBottom(); } protected void createChatStateTimerTask() { typingTimerTask = new TimerTask() { @Override public void run() { final long lastUpdate = System.currentTimeMillis() - lastNotificationSentTime; switch ( lastNotificationSent ) { case paused: case active: if ( lastUpdate > inactiveTimePeriod ) { setChatState( ChatState.inactive ); } break; case composing: if ( lastUpdate > pauseTimePeriod ) { setChatState( ChatState.paused ); } break; } } }; TaskEngine.getInstance().scheduleAtFixedRate(typingTimerTask, pauseTimePeriod /2, pauseTimePeriod / 2); } /** * Sends a chat state to all peers. * * @param state the chat state. * @throws InterruptedException */ protected abstract void sendChatState( ChatState state ) throws SmackException.NotConnectedException, InterruptedException; /** * Sets the chat state, causing an update to be sent to all peers if the new state warrants an update. * * @param state the chat state (never null). */ public final void setChatState(ChatState state) { if ( state == null ) { throw new IllegalArgumentException( "Argument 'state' cannot be null." ); } // Only sent out a chat state notification when it is different from the last one that was transmitted... final boolean isDifferentState = lastNotificationSent != state; // ... unless it's 'composing' - that can be repeated every so many seconds. final boolean isStillComposing = state == ChatState.composing && System.currentTimeMillis() - lastNotificationSentTime > 2000; final long now = System.currentTimeMillis(); if ( isDifferentState || isStillComposing ) { try { sendChatState( state ); } catch ( SmackException.NotConnectedException | InterruptedException e ) { Log.warning( "Unable to update the chat state to " + state, e ); } lastNotificationSent = state; lastNotificationSentTime = now; } } /** * Handles the Nickname Completion dialog, when Pressing CTRL + SPACE<br> * it searches for matches in the current GroupchatList and also in the * Roster * * @throws ChatRoomNotFoundException * when for some reason the GroupChatRoom cannot be found, this * should <u>not</u> happen, since we retrieve it from the * ActiveWindowTab and thus <u>can be ignored</u> */ private void handleNickNameCompletion() throws ChatRoomNotFoundException { // Search for a name that starts with the same word as the last word in the chat input editor. final String text = getChatInputEditor().getText(); if ( text == null || text.isEmpty() ) { return; } final int lastSpaceCharacterIndex = text.lastIndexOf( ' ' ); // -1 when space does not occur. final String needle = text.substring( lastSpaceCharacterIndex + 1 ); final Set<String> matches = new TreeSet<>( String::compareToIgnoreCase ); if ( SparkManager.getChatManager().getChatContainer().getActiveChatRoom() instanceof GroupChatRoom ) { final GroupChatRoom activeChatRoom = (GroupChatRoom) SparkManager.getChatManager().getChatContainer().getActiveChatRoom(); for ( EntityFullJid participant : activeChatRoom.getParticipants() ) { final Resourcepart nickname = participant.getResourcepart(); if ( nickname.toString().toLowerCase().startsWith( needle.toLowerCase() ) ) { matches.add( nickname.toString() ); } } } else { for ( RosterEntry re : Roster.getInstanceFor( SparkManager.getConnection() ).getEntries() ) { // Use the name if available, otherwise the localpart of the JID. final String username; if ( re.getName() != null ) { username = re.getName(); } else { username = re.getUser().substring( 0, re.getUser().indexOf( '@' ) ); } if ( username.toLowerCase().startsWith( needle.toLowerCase() ) ) { matches.add( username ); } } } if ( matches.size() == 1 ) { // If we only have 1 match, that match can be used immediately. getChatInputEditor().appendText( matches.iterator().next().substring( needle.length() ) ); } else { // More than one match: create Popupmenu and let the user select one. final JPopupMenu popup = new JPopupMenu(); for ( final String match : matches ) { final JMenuItem menuItem = new JMenuItem( match ); popup.add( menuItem ); menuItem.addActionListener( new AbstractAction() { @Override public void actionPerformed( ActionEvent e ) { getChatInputEditor().appendText( match.substring( needle.length() ) ); popup.setVisible( false ); } } ); } popup.show( SparkManager.getChatManager().getChatContainer(), getChatInputEditor().getCaret().getMagicCaretPosition().x, SparkManager.getChatManager().getChatContainer().getHeight() - 20 ); } } // I would normally use the command pattern, but // have no real use when dealing with just a couple options. @Override public void actionPerformed(ActionEvent e) { sendMessage(); // Clear send field and disable send button getChatInputEditor().clear(); chatAreaButton.getButton().setEnabled(false); } /** * Creates and sends a message object from the text in * the Send Field, using the default nickname specified in your * Chat Preferences. */ protected abstract void sendMessage(); /** * Creates a Message object from the given text and delegates to the room * for sending. * * @param text the text to send. */ protected abstract void sendMessage(String text); /** * Sends the current message. * * @param message - the message to send. */ public abstract void sendMessage(Message message); /** * Returns the nickname of the current agent as specified in Chat * Preferences. * * @return the nickname of the agent. */ public Resourcepart getNickname() { LocalPreferences pref = SettingsManager.getLocalPreferences(); return pref.getNickname(); } /** * The main entry point when receiving any messages. This will * either handle a message from a customer or delegate itself * as an agent handler. * * @param message - the message receieved. */ public void insertMessage(Message message) { // Fire Message Filters SparkManager.getChatManager().filterIncomingMessage(this, message); SparkManager.getChatManager().fireGlobalMessageReceievedListeners(this, message); addToTranscript(message, true); fireMessageReceived(message); SparkManager.getWorkspace().getTranscriptPlugin().persistChatRoom(this); } /** * Add a <code>ChatResponse</chat> to the current discussion chat area. * * @param message the message to add to the transcript list * @param updateDate true if you wish the date label to be updated with the * date and time the message was received. */ public void addToTranscript(Message message, boolean updateDate) { // Create message to persist. final Message newMessage = new Message(); newMessage.setTo(message.getTo()); newMessage.setFrom(message.getFrom()); newMessage.setBody(message.getBody()); final Map<String, Object> properties = new HashMap<>(); properties.put( "date", new Date() ); newMessage.addExtension( new JivePropertiesExtension( properties ) ); transcript.add(newMessage); // Add current date if this is the current agent if (updateDate && transcriptWindow.getLastUpdated() != null) { // Set new label date notificationLabel.setIcon(SparkRes.getImageIcon(SparkRes.SMALL_ABOUT_IMAGE)); notificationLabel.setText(Res.getString("message.last.message.received", SparkManager.DATE_SECOND_FORMATTER.format(transcriptWindow.getLastUpdated()))); } scrollToBottom(); } /** * Adds a new message to the transcript history. * * @param to who the message is to. * @param from who the message was from. * @param body the body of the message. * @param date when the message was received. */ public void addToTranscript(String to, String from, String body, Date date) { final Message newMessage = new Message(); newMessage.setTo(to); newMessage.setFrom(from); newMessage.setBody(body); final Map<String, Object> properties = new HashMap<>(); properties.put( "date", new Date() ); newMessage.addExtension( new JivePropertiesExtension( properties ) ); transcript.add(newMessage); } /** * Scrolls the chat window to the bottom. */ public void scrollToBottom() { if (mousePressed) { return; } int lengthOfChat = transcriptWindow.getDocument().getLength(); transcriptWindow.setCaretPosition(lengthOfChat); try { final JScrollBar scrollBar = textScroller.getVerticalScrollBar(); EventQueue.invokeLater( () -> scrollBar.setValue(scrollBar.getMaximum()) ); } catch (Exception e) { Log.error(e); } } /** * Checks to see if the Send button should be enabled. * * @param e - the documentevent to react to. */ protected void checkForText(DocumentEvent e) { final int length = e.getDocument().getLength(); if (length > 0) { chatAreaButton.getButton().setEnabled(true); } else { chatAreaButton.getButton().setEnabled(false); } } /** * Requests valid focus to the SendField. */ public void positionCursor() { getChatInputEditor().setCaretPosition(getChatInputEditor().getCaretPosition()); chatAreaButton.getChatInputArea().requestFocusInWindow(); } /** * Disable the chat room. This is called when a chat has been either transfered over or * the customer has left the chat room. */ public abstract void leaveChatRoom(); /** * Process incoming packets. * * @param stanza - the packet to process */ public void processPacket(Stanza stanza) { } /** * Returns the SendField component. * * @return the SendField ChatSendField. */ public ChatInputEditor getChatInputEditor() { return chatAreaButton.getChatInputArea(); } /** * Returns the chatWindow components. * * @return the ChatWindow component. */ public TranscriptWindow getTranscriptWindow() { return transcriptWindow; } /** * Checks to see if enter was pressed and validates room. * * @param e the KeyEvent */ private void checkForEnter(KeyEvent e) { final KeyStroke keyStroke = KeyStroke.getKeyStroke(e.getKeyCode(), e.getModifiers()); if (!keyStroke.equals(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.SHIFT_DOWN_MASK)) && e.getKeyChar() == KeyEvent.VK_ENTER) { e.consume(); sendMessage(); getChatInputEditor().setText(""); getChatInputEditor().setCaretPosition(0); SparkManager.getWorkspace().getTranscriptPlugin().persistChatRoom(this); } else if (keyStroke.equals(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.SHIFT_DOWN_MASK))) { final Document document = getChatInputEditor().getDocument(); try { document.insertString(getChatInputEditor().getCaretPosition(), "\n", null); getChatInputEditor().requestFocusInWindow(); chatAreaButton.getButton().setEnabled(true); } catch (BadLocationException badLoc) { Log.error("Error when checking for enter:", badLoc); } } } /** * Add a {@link MessageListener} to the current ChatRoom. * * @param listener - the MessageListener to add to the current ChatRoom. */ public void addMessageListener(MessageListener listener) { messageListeners.add(listener); } /** * Remove the specified {@link MessageListener } from the current ChatRoom. * * @param listener - the MessageListener to remove from the current ChatRoom. */ public void removeMessageListener(MessageListener listener) { messageListeners.remove(listener); } /** * Notifies all message listeners that * * @param message the message received. */ private void fireMessageReceived( Message message ) { for ( MessageListener listener : messageListeners ) { try { listener.messageReceived( this, message ); } catch ( Exception e ) { Log.error( "A MessageListener (" + listener + ") threw an exception while processing a 'message received' event for message: " + message, e ); } } } /** * Notifies all <code>MessageListener</code> that a message has been sent. * * @param message the message sent. */ protected void fireMessageSent( Message message ) { for ( MessageListener listener : messageListeners ) { try { listener.messageSent( this, message ); } catch ( Exception e ) { Log.error( "A MessageListener (" + listener + ") threw an exception while processing a 'message sent' event for message: " + message, e ); } } } /** * Returns a map of the current Chat Transcript which is a list of all * ChatResponses and their order. You should retrieve this map to get * any current chat transcript state. * * @return - the map of current chat responses. */ public List<Message> getTranscripts() { return transcript; } /** * Disables the ChatRoom toolbar. */ public void disableToolbar() { final int count = editorBarLeft.getComponentCount(); for (int i = 0; i < count; i++) { final Object o = editorBarLeft.getComponent(i); if (o instanceof RolloverButton) { final RolloverButton rb = (RolloverButton)o; rb.setEnabled(false); } } } /** * Enable the ChatRoom toolbar. */ public void enableToolbar() { final int count = editorBarLeft.getComponentCount(); for (int i = 0; i < count; i++) { final Object o = editorBarLeft.getComponent(i); if (o instanceof RolloverButton) { final RolloverButton rb = (RolloverButton)o; rb.setEnabled(true); } } } /** * Checks to see if the Send Button should be enabled depending on the * current update in SendField. * * @param event the DocumentEvent from the sendField. */ @Override public void removeUpdate(DocumentEvent event) { checkForText(event); } /** * Checks to see if the Send button should be enabled. * * @param docEvent the document event. */ @Override public void changedUpdate(DocumentEvent docEvent) { // Do nothing. } /** * Return the splitpane used in this chat room. * * @return the splitpane used in this chat room. */ public JSplitPane getSplitPane() { return splitPane; } /** * Returns the ChatPanel that contains the ChatWindow and SendField. * * @return the ChatPanel. */ public JPanel getChatPanel() { return chatPanel; } /** * Close the ChatRoom. */ public void closeChatRoom() { fireClosingListeners(); setChatState(ChatState.gone); if (typingTimerTask != null) { TaskEngine.getInstance().cancelScheduledTask(typingTimerTask); typingTimerTask = null; } getTranscriptWindow().removeContextMenuListener(this); getTranscriptWindow().removeMouseListener(transcriptWindowMouseListener); getChatInputEditor().removeKeyListener(chatEditorKeyListener); this.removeAll(); textScroller.getViewport().remove(transcriptWindow); // Remove Connection Listener SparkManager.getConnection().removeConnectionListener(this); getTranscriptWindow().setTransferHandler(null); getChatInputEditor().setTransferHandler(null); transferHandler = null; packetIDList.clear(); messageListeners.clear(); fileDropListeners.clear(); getChatInputEditor().close(); getChatInputEditor().getActionMap().remove("closeTheRoom"); chatAreaButton.getButton().removeActionListener(this); bottomPanel.remove(chatAreaButton); // TODO: We are seeing NPEs in the next line. Find out why _chatFrame is null. _chatFrame.removeWindowToFrontListener(this); } /** * Get the <code>Icon</code> to be used in the tab holding * this ChatRoom. * * @return - <code>Icon</code> to use */ public abstract Icon getTabIcon(); /** * Get the roomname to use for this ChatRoom. This is expected to be a bare jid. * * @return - the Roomname of this ChatRoom. * @deprecated use {@link #getRoomJid()} instead. */ @Deprecated public EntityBareJid getRoomname() { return getRoomJid(); } /** * Get the XMPP address of this room. * * @return the XMPP address of this room */ public abstract EntityBareJid getRoomJid(); /** * Get the title to use in the tab holding this ChatRoom. * * @return - the title to use. */ public abstract String getTabTitle(); /** * Returns the title of this room to use. The title * will be used in the title bar of the ChatRoom. * * @return - the title of this ChatRoom. */ public abstract String getRoomTitle(); /** * Returns the <code>Message.Type</code> specific to this * chat room. * GroupChat is Message.Type.groupchat * Normal Chat is Message.TYPE.NORMAL * * @return the ChatRooms Message.TYPE */ public abstract Message.Type getChatType(); /** * Returns whether or not this ChatRoom is active. Note: carrying * a conversation rather than being disabled, as it would be * transcript mode. * * @return true if the chat room is active. */ public abstract boolean isActive(); /** * Returns the notification label. The notification label notifies the * user of chat room activity, such as the date of the last message * and typing notifications. * * @return the notification label. */ public JLabel getNotificationLabel() { return notificationLabel; } /** * Adds a packetID to the packedIDList. The packetIDLlist * keeps track of all messages coming into the chatroom. * * @param packetID the packetID to add. */ public void addPacketID(String packetID) { packetIDList.add(packetID); } /** * Checks if the packetID has already been used. * * @param packetID the packetID to check for. * @return true if the packetID already exists. */ public boolean packetIDExists(String packetID) { return packetIDList.contains(packetID); } /** * Returns this instance of the chatroom. * * @return the current ChatRoom instance. */ public ChatRoom getChatRoom() { return this; } /** * Returns the toolbar used on top of the chat room. * * @return the toolbar used on top of this chat room. */ public ChatToolBar getToolBar() { return toolbar; } protected void addToolbar() { add(toolbar, new GridBagConstraints(0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0)); } @Override public void insertUpdate(DocumentEvent e) { // Meant to be overriden checkForText(e); setChatState( ChatState.composing ); } /** * Override to save transcript in preferred room style. */ public void saveTranscript() { getTranscriptWindow().saveTranscript(getTabTitle() + ".html", getTranscripts(), null); } /** * Used for the top toolbar. */ public class ChatToolBar extends JPanel { private static final long serialVersionUID = 5926527530611601841L; private JPanel buttonPanel; /** * Default Constructor. */ public ChatToolBar() { buttonPanel = new JPanel(); buttonPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 2, 0)); // Set Layout setLayout(new GridBagLayout()); buttonPanel.setOpaque(false); add(buttonPanel, new GridBagConstraints(1, 1, 1, 1, 0.0, 0.0, GridBagConstraints.NORTHEAST, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0)); setOpaque(false); } /** * Adds a new ChatRoomButton the CommandBar. * * @param button the button. */ public void addChatRoomButton(ChatRoomButton button) { buttonPanel.add(button); // Make all JButtons the same size Component[] comps = buttonPanel.getComponents(); final int no = comps != null ? comps.length : 0; final List<Component> buttons = new ArrayList<>(); for (int i = 0; i < no; i++) { try { Component component = comps[i]; if (component instanceof JButton) { buttons.add(component); } } catch (NullPointerException e) { Log.error(e); } } GraphicUtils.makeSameSize(buttons.toArray(new JComponent[buttons.size()])); } /** * Removes the ChatRoomButton from the CommandBar. * * @param button the button. */ public void removeChatRoomButton(ChatRoomButton button) { buttonPanel.remove(button); } } /** * Returns the number of unread messages in this ChatRoom. * * @return the number of unread messages. */ public int getUnreadMessageCount() { return unreadMessageCount; } /** * Increases the number of unread messages by 1. */ public void increaseUnreadMessageCount() { unreadMessageCount++; } /** * Resets the number of unread messages. */ public void clearUnreadMessageCount() { unreadMessageCount = 0; } /** * Returns the bottom panel used in the ChatRoom. * * @return the bottomPane; */ public JPanel getBottomPanel() { return bottomPanel; } /** * Returns the Container which holds the ChatWindow. * * @return the Container. */ public JPanel getChatWindowPanel() { return chatWindowPanel; } /** * Adds a new <code>FileDropListener</code> to allow for Drag and Drop notifications * of objects onto the ChatWindow. * * @param listener the listener. */ public void addFileDropListener(FileDropListener listener) { fileDropListeners.add(listener); } /** * Remove the <code>FileDropListener</code> from ChatRoom. * * @param listener the listener. */ public void removeFileDropListener(FileDropListener listener) { fileDropListeners.remove(listener); } /** * Notify all users that a collection of files has been dropped onto the ChatRoom. * * @param files the files dropped. */ public void fireFileDropListeners( Collection<File> files ) { for ( FileDropListener listener : fileDropListeners ) { try { listener.filesDropped( files, this ); } catch ( Exception e ) { Log.error( "A FileDropListener (" + listener + ") threw an exception while processing a 'files dropped' event.", e ); } } } /** * Returns the panel which contains the toolbar items, such as spell checker. * * @return the panel which contains the lower toolbar items. */ public JPanel getEditorBar() { return editorBarLeft; } /** * Returns the panel next to the editor bar<br> * for use with system buttons, like room controlling or toggle stay-on-top * * @return */ public JPanel getRoomControllerBar() { return editorBarRight; } /** * Adds a <code>ChatRoomClosingListener</code> to this ChatRoom. A ChatRoomClosingListener * is notified whenever this room is closing. * * @param listener the ChatRoomClosingListener. */ public void addClosingListener(ChatRoomClosingListener listener) { closingListeners.add( listener ); } /** * Removes a <code>ChatRoomClosingListener</code> from this ChatRoom. * * @param listener the ChatRoomClosingListener. */ public void removeClosingListener(ChatRoomClosingListener listener) { closingListeners.remove( listener ); } /** * Notifies all <code>ChatRoomClosingListener</code> that this ChatRoom is closing. */ private void fireClosingListeners() { for ( final ChatRoomClosingListener listener : new ArrayList<>( closingListeners ) ) // Listener can call #removeClosingListener. Prevent ConcurrentModificationException by using a clone. { try { listener.closing(); } catch ( Exception e ) { Log.error( "A ChatRoomClosingListener (" + listener + ") threw an exception while processing a 'closing' event.", e ); } } closingListeners.clear(); } /** * Returns the ScrollPane that contains the TranscriptWindow. * * @return the <code>TranscriptWindow</code> ScrollPane. */ public JScrollPane getScrollPaneForTranscriptWindow() { return textScroller; } /** * Return the "Send" button. * * @return the send button. */ public JButton getSendButton() { return chatAreaButton.getButton(); } /** * Returns the VerticalSplitPane used in this ChatRoom. * * @return the VerticalSplitPane. */ public JSplitPane getVerticalSlipPane() { return verticalSplit; } @Override public void focusGained(FocusEvent focusEvent) { validate(); invalidate(); repaint(); if(focusEvent.getComponent().equals(getChatInputEditor())) { setChatState( ChatState.active ); } } @Override public void poppingUp(Object component, JPopupMenu popup) { Action saveAction = new AbstractAction() { private static final long serialVersionUID = -3582301239832606653L; @Override public void actionPerformed(ActionEvent actionEvent) { saveTranscript(); } }; saveAction.putValue(Action.NAME, Res.getString("action.save")); saveAction.putValue(Action.SMALL_ICON, SparkRes.getImageIcon(SparkRes.SAVE_AS_16x16)); popup.add(saveAction); } @Override public void poppingDown(JPopupMenu popup) { } @Override public boolean handleDefaultAction(MouseEvent e) { return false; } @Override public void focusLost(FocusEvent focusEvent) { if(focusEvent.getComponent().equals(getChatInputEditor())) { setChatState( ChatState.inactive ); } } /** * Implementation of this method should return the last time this chat room * sent or recieved a message. * * @return the last time (in system milliseconds) that the room last recieved a message. */ public abstract long getLastActivity(); @Override public void connectionClosed() { } @Override public void connectionClosedOnError(Exception e) { } @Override public void updateStatus(boolean active) { _alwaysOnTopItem.setSelected(active); } @Override public void registeredToFrame(ChatFrame chatframe) { this._chatFrame = chatframe; _chatFrame.addWindowToFronListener(this); } protected JPanel getEditorWrapperBar() { return editorWrapperBar; } protected JPanel getEditorBarRight() { return editorBarRight; } protected JPanel getEditorBarLeft() { return editorBarLeft; } protected JScrollPane getTextScroller() { return textScroller; } protected Insets getChatPanelInsets() { return new Insets(0, 5, 0, 5); } protected Insets getChatAreaInsets() { return new Insets(0, 5, 5, 5); } protected Insets getEditorWrapperInsets() { return new Insets(0, 5, 0, 5); } public void addChatRoomComponent(JComponent component) { editorBarLeft.add(component); } public void addChatRoomButton(ChatRoomButton button) { addChatRoomButton(button, false); } public void addChatRoomButton(ChatRoomButton button, boolean forceRepaint) { toolbar.addChatRoomButton(button); if (forceRepaint) { toolbar.invalidate(); toolbar.repaint(); } } public void showToolbar() { toolbar.setVisible(true); } public void hideToolbar() { toolbar.setVisible(false); } public void addEditorComponent(JComponent component) { editorBarLeft.add(component); } public void removeEditorComponent(JComponent component) { editorBarLeft.remove(component); } public void addControllerButton(RolloverButton button) { editorBarRight.add(button, 0); } }