001    /*
002            Copyright (C) 2003 Adam Olsen
003    
004            This program is free software; you can redistribute it and/or modify
005            it under the terms of the GNU General Public License as published by
006            the Free Software Foundation; either version 1, or (at your option)
007            any later version.
008    
009            This program is distributed in the hope that it will be useful,
010            but WITHOUT ANY WARRANTY; without even the implied warranty of
011            MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012            GNU General Public License for more details.
013    
014            You should have received a copy of the GNU General Public License
015            along with this program; if not, write to the Free Software
016            Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
017    */
018    
019    package com.valhalla.jbother;
020    
021    import java.awt.*;
022    import java.awt.event.*;
023    import java.net.URL;
024    import java.util.*;
025    import java.text.*;
026    
027    import javax.swing.*;
028    import javax.swing.event.*;
029    import java.beans.*;
030    
031    import org.jivesoftware.smack.packet.Message;
032    import org.jivesoftware.smack.packet.Presence;
033    
034    import com.valhalla.gui.*;
035    import com.valhalla.jbother.*;
036    import com.valhalla.jbother.menus.ConversationPopupMenu;
037    import com.valhalla.jbother.jabber.BuddyStatus;
038    import com.valhalla.settings.Settings;
039    
040    /**
041     * Handles conversations between two users.
042     * It is usually associated with a BuddyStatus.
043     *
044     * @author Adam Olsen
045     * @version 1.0
046     * @see com.valhalla.jbother.jabber.BuddyStatus
047     **/
048    public class ChatPanel extends ConversationPanel
049    {
050            private ResourceBundle resources = ResourceBundle.getBundle( "JBotherBundle", Locale.getDefault() );
051            //private StringBuffer conversationText = new StringBuffer();
052            private boolean               offlineMessage = false;
053            private ConversationPopupMenu popMenu = new ConversationPopupMenu( this, conversationArea );
054    
055            //private HTMLDocument document = (HTMLDocument)conversationArea.getDocument();
056            private JSplitPane  container;
057            private JScrollPane     conversationScroll;
058            private JPanel      buttonPanel   = new JPanel();
059            private JPanel      scrollPanel   = new JPanel( new GridLayout( 1, 0 ) );
060            private JTextArea   textEntryArea = new JTextArea();
061            //private JCheckBox useHTML = new JCheckBox( "Use HTML" );
062            // for logging
063            private JComboBox resourceBox = new JComboBox();
064            private JButton clearButton = new JButton( Standard.getIcon( "images/buttons/New24.gif" ) );
065            private JButton logButton = new JButton( Standard.getIcon( "images/buttons/Log24.gif" ) );
066            private JButton emoteButton = new JButton( Standard.getIcon( "images/buttons/smiley.gif" ) );
067            private ChatPanel thisPointer = this;
068            private boolean divSetUp = false;
069    
070            /**
071             * Sets up the ChatPanel - creates all visual components and adds event listeners
072             * @param buddy the buddy to associate with
073             * @param userId the buddy's userId
074             * @param buddyName the buddy's alias
075            */
076            public ChatPanel( BuddyStatus buddy )
077            {
078                    super( buddy );
079    
080                    setLayout( new BoxLayout( this, BoxLayout.Y_AXIS ) );
081                    setBorder( BorderFactory.createEmptyBorder( 5, 5, 5, 5 ) );
082    
083                    //create two fields, one for where you type your message to be sent, and the
084                    //other where you see the conversation that has already happened.
085                    textEntryArea.setLineWrap( true );
086                    textEntryArea.setWrapStyleWord( true );
087    
088                    conversationScroll = new JScrollPane( conversationArea );
089                    conversationScroll.setVerticalScrollBarPolicy( JScrollPane.VERTICAL_SCROLLBAR_ALWAYS );
090                    conversationArea.setScroll( conversationScroll );
091    
092                    scrollPanel.add( conversationScroll );
093    
094                    container = new JSplitPane( JSplitPane.VERTICAL_SPLIT, scrollPanel, new JScrollPane( textEntryArea ) );
095                    container.setResizeWeight( 1 );
096    
097                    JPanel containerPanel = new JPanel();
098                    containerPanel.setLayout( new BoxLayout( containerPanel, BoxLayout.X_AXIS ) );
099                    containerPanel.add( container );
100    
101                    JPanel bottomPanel = new JPanel();
102                    bottomPanel.setLayout( new BoxLayout( bottomPanel, BoxLayout.Y_AXIS ) );
103    
104                    if( buddy.getUser().indexOf( "/" ) >= 0 ) resourceBox.setEnabled( false );
105                    resourceBox.setRenderer( new PresenceComboBoxRenderer() );
106                    buttonPanel.setLayout( new BoxLayout( buttonPanel, BoxLayout.X_AXIS ) );
107    
108                    JPanel resourcePanel = new JPanel();
109                    resourcePanel.setLayout( new BoxLayout( resourcePanel, BoxLayout.Y_AXIS ) );
110                    resourcePanel.add( Box.createVerticalGlue() );
111                    resourcePanel.add( resourceBox );
112    
113                    buttonPanel.add( resourcePanel );
114                    buttonPanel.add( Box.createRigidArea( new Dimension( 5, 0 ) ) );
115                    emoteButton.setPreferredSize( new Dimension( 26, 26 ) );
116                    buttonPanel.add( emoteButton );
117                    clearButton.setPreferredSize( new Dimension( 26, 26 ) );
118                    buttonPanel.add( clearButton );
119                    logButton.setPreferredSize( new Dimension( 26, 26 ) );
120                    buttonPanel.add( logButton );
121    
122                    bottomPanel.add( buttonPanel );
123    
124                    add( containerPanel );
125                    add( Box.createRigidArea( new Dimension( 0, 5 ) ) );
126                    add( bottomPanel );
127    
128                    textEntryArea.grabFocus();
129    
130                    addListeners();
131                    updateResources();
132            }
133    
134            /**
135             * @return the input area of this panel
136            */
137            public Component getInputComponent() { return textEntryArea; }
138    
139            /**
140             * Sets up the Divider
141            */
142            public void setUpDivider()
143            {
144                    if( divSetUp ) return;
145                    divSetUp = true;
146                    String stringHeight = Settings.getInstance().getProperty( "conversationWindowHeight" );
147    
148                    // set up the divider location from settings
149                    String divLocString = Settings.getInstance().getProperty( "conversationWindowDividerLocation" );
150                    int divLoc = 0;
151    
152                    if( divLocString != null )
153                    {
154                            divLoc = Integer.parseInt( divLocString );
155                    }
156                    else {
157                            divLoc = Integer.parseInt( stringHeight ) - 117;
158                    }
159    
160                    container.setDividerLocation( divLoc );
161                    container.validate();
162                    container.addPropertyChangeListener( "lastDividerLocation", new DividerListener() );
163            }
164    
165            /**
166             * Focuses when a Tab in the TabFrame is clicked
167            */
168            public void focusYourself()
169            {
170                    textEntryArea.grabFocus();
171            }
172    
173            /**
174             * @return the ChatPanel's JSPlitPane
175            */
176            public JSplitPane getSplitPane() { return container; }
177    
178            /**
179             * Listens for the user to move the divider, and saves it's location
180             * @author Adam Olsen
181             * @version 1.0
182            */
183            private class DividerListener implements PropertyChangeListener
184            {
185                    public void propertyChange( PropertyChangeEvent e )
186                    {
187                            Settings.getInstance().setProperty( "conversationWindowDividerLocation", e.getOldValue().toString() );
188                    }
189            }
190    
191            /**
192             * Updates the ConversationArea's font
193             * @param font the font to update to
194            */
195            public void updateStyle( Font font ) { conversationArea.loadStyleSheet( font ); }
196    
197            /**
198             * Gets the ComboBox with all the buddy's resources
199             * @return the ComboBox
200            */
201            public JComboBox getResourceBox() { return resourceBox; }
202    
203            /**
204             * Updates the JComboBox with the buddy's current resources
205            **/
206            public void updateResources()
207            {
208                    SwingUtilities.invokeLater( new Runnable()
209                    {
210                            public void run()
211                            {
212                                    String selected = (String)resourceBox.getSelectedItem();
213                                    if( selected == null ) selected = "";
214    
215                                    resourceBox.removeAllItems();
216                                    resourceBox.addItem( resources.getString( "defaultResource" ) );
217                                    resourceBox.addItem( resources.getString( "allResources" ) );
218    
219                                    if( buddy.getUser().indexOf( "/" ) >= 0 ) return;
220    
221                                    Iterator i = buddy.keySet().iterator();
222    
223                                    int count = 2, sel = 0;
224                                    while( i.hasNext() )
225                                    {
226                                            String key = (String)i.next();
227                                            if( !key.equals( "N/A" ) )
228                                            {
229                                                    resourceBox.addItem( key );
230                                                    if( key.equals( selected ) ) sel = count;
231                                                    count++;
232                                            }
233                                    }
234    
235                                    if( count == 2 )
236                                    {
237                                            sel = 0;
238                                            resourceBox.setEnabled( false );
239                                    }
240                                    else if( count == 3 )
241                                    {
242                                            sel = 2;
243                                            resourceBox.setEnabled( false );
244                                    }
245                                    else resourceBox.setEnabled( true );
246    
247                                    if( selected.equals( resources.getString( "allResources" ) ) ) sel = 1;
248                                    if( sel > 0 && sel <= resourceBox.getItemCount() ) resourceBox.setSelectedIndex( sel );
249                                    else resourceBox.setSelectedIndex( 0 );
250                                    resourceBox.repaint();
251                            }
252                    } );
253            }
254    
255            /**
256             * Adds the various event listeners for the components that are a part of this
257             * frame
258             **/
259            private void addListeners()
260            {
261                    clearButton.setToolTipText( resources.getString( "clear" ) );
262                    logButton.setToolTipText( resources.getString( "viewLog" ) );
263                    emoteButton.addActionListener( new ActionListener()
264                    {
265                            public void actionPerformed( ActionEvent e )
266                            {
267                                    JFrame f = frame;
268                                    if( f == null ) f = BuddyList.getInstance().getTabFrame();
269                                    Emoticons.getInstance().displayEmoticonChooser( f, emoteButton, textEntryArea );
270                            }
271                    } );
272    
273                    clearButton.addActionListener( new ActionListener()
274                    {
275                            public void actionPerformed( ActionEvent e ) { conversationArea.setText( "" ); }
276                    } );
277    
278                    logButton.addActionListener( new ActionListener()
279                    {
280                            public void actionPerformed( ActionEvent e ) { openLogWindow(); }
281                    } );
282    
283    
284                    //if the press enter, send the message
285                    Action sendMessageAction = new AbstractAction()
286                    {
287                            public void actionPerformed( ActionEvent e ) { sendHandler(); }
288                    };
289    
290                    Action shiftEnterAction = new AbstractAction()
291                    {
292                            public void actionPerformed( ActionEvent e )
293                            {
294                                    int pos = textEntryArea.getCaretPosition();
295                                    if( pos < 0 ) pos = 0;
296    
297                                    textEntryArea.setText( textEntryArea.getText() + "\n" );
298    
299                                    try {
300                                            textEntryArea.setCaretPosition( pos + 1 );
301                                    }
302                                    catch( IllegalArgumentException ex ) { }
303                            }
304                    };
305    
306                    Action checkCloseAction = new AbstractAction()
307                    {
308                            public void actionPerformed( ActionEvent e ) { checkCloseHandler(); }
309                    };
310    
311                    Action closeAction = new AbstractAction()
312                    {
313                            public void actionPerformed( ActionEvent e ) { closeHandler(); }
314                    };
315    
316                    //for making it so they can select text in the window and not have it lose focus on them
317                    conversationArea.addMouseMotionListener( new MouseMotionAdapter()
318                    {
319                            public void mouseDragged( MouseEvent e ) { conversationArea.grabFocus(); }
320                    } );
321    
322                    conversationArea.addFocusListener( new FocusListener()
323                    {
324                            public void focusGained( FocusEvent e )
325                            {
326                                    //only request focus to the input box if there is no text selected
327                                    if( conversationArea.getSelectedText() == null ) textEntryArea.requestFocus();
328                                    else conversationArea.grabFocus();
329                            }
330    
331                            public void focusLost( FocusEvent e ) { }
332                    } );
333    
334                    conversationArea.addMouseListener( new RightClickListener( popMenu ) );
335    
336                    textEntryArea.getInputMap().put( KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, 0 ), sendMessageAction );
337                    textEntryArea.getInputMap().put( KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, java.awt.event.InputEvent.SHIFT_MASK ), shiftEnterAction );
338                    textEntryArea.getInputMap().put( KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() ), shiftEnterAction );
339                    textEntryArea.getInputMap().put( KeyStroke.getKeyStroke( KeyEvent.VK_W, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() ), checkCloseAction );
340                    textEntryArea.getInputMap().put( KeyStroke.getKeyStroke( KeyEvent.VK_K, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() ), closeAction );
341            }
342    
343            /**
344             * Displays a little blurb that the last messaged recieved was from an offline buddy
345            */
346            public void setOfflineMessage()
347            {
348                    if( !offlineMessage )
349                    {
350                            conversationArea.setText( "<b>" + resources.getString( "offlineMessage" ) + ":</b><hr width='100%'>" );
351                            offlineMessage = true;
352                    }
353            }
354    
355            /**
356             * Displays a message in the window when the buddy signs off
357            */
358            public void signedOff()
359            {
360                    conversationArea.append( "<font color='green'>" + getDate() + "<b> " +
361                    buddy.getName() + " " + resources.getString( "signedOff" ) + "</b></font><br>" );
362                    if( logOut != null )
363                    {
364                            logOut.println( conversationArea.getLastHTML() );
365                    }
366            }
367    
368            /**
369             * Displays a message in the window when a buddy signs on
370            */
371            public void signedOn()
372            {
373                    conversationArea.append( "<font color='green'>" + getDate() + "<b> " +
374                    buddy.getName() + " " + resources.getString( "signedOn" ) + "</b></font><br>" );
375                    if( logOut != null )
376                    {
377                            logOut.println( conversationArea.getLastHTML() );
378                    }
379            }
380    
381            /**
382             * Displays a "disconnected" message"
383            */
384            public void disconnected()
385            {
386                    conversationArea.append( "<b>" + getDate() + " **** " + resources.getString( "disconnected" ) );
387            }
388    
389            /**
390             * Recieves a message
391             * @param sbj the message subject
392             * @param body the message body
393             * @param resource the resource the message came from if there is one
394            */
395            public void recieveMessage( String sbj, String body, final String resource )
396            {
397                    SwingUtilities.invokeLater( new Runnable()
398                    {
399                            public void run()
400                            {
401                                    if( resource != null && buddy.getUser().indexOf( "/" ) < 0 ) resourceBox.setSelectedItem( resource );
402                                    resourceBox.validate();
403                            }
404                    } );
405    
406                    body = ConversationText.replaceText( body, false );
407    
408                    if( body.startsWith( " /me " ) )
409                    {
410                            body = body.replaceAll( "^ \\/me ", "" );
411                            conversationArea.append( getDate() + " <b>* " +
412                                    buddy.getName() + "</b> " + body +   "<br>" );
413                    }
414                    else {
415                            conversationArea.append( "<font color='#16569e'>" + getDate() + " <b>" + buddy.getName() + "</b></font>: " + body + "<br>" );
416                    }
417                    if( logOut != null ) logOut.println( conversationArea.getLastHTML() );
418    
419                    super.recieveMessage();
420                    setUpDivider();
421            }
422    
423            /**
424             * Sends the message in the TextEntryArea
425            **/
426            private void sendHandler()
427            {
428                    if( !textEntryArea.getText().equals( "" ) )
429                    {
430                            if( !BuddyList.getInstance().checkConnection() )
431                            {
432                                    BuddyList.getInstance().connectionError();
433                                    return;
434                            }
435    
436                            sendBuddyMessage( textEntryArea.getText() );
437    
438                            String text = ConversationText.replaceText( textEntryArea.getText(), false );
439    
440                            if( text.startsWith( " /me " ) )
441                            {
442                                    text = text.replaceAll( "^ \\/me ", "" );
443                                    conversationArea.append( getDate() + " <b>* " +
444                                            BuddyList.getInstance().getMyName() + "</b> " + text + "</font><br>" );
445                            }
446                            else {
447                                    conversationArea.append( "<font color='maroon'>" + getDate() + " <b>" +
448                                                            BuddyList.getInstance().getMyName() + "</b></font>: " + text + "<br>" );
449                            }
450    
451                            textEntryArea.setText( "" );
452                            //com.valhalla.jbother.sound.SoundPlayer.play( "sentSound" );
453                            if( logOut != null ) logOut.println( conversationArea.getLastHTML() );
454                    }
455            }
456    
457            /**
458             * Sends the message to the resource in the JComboBox
459             * @param text the message to send
460            */
461            public void sendBuddyMessage( String text )
462            {
463                    String to = buddy.getUser();
464                    int sel = resourceBox.getSelectedIndex();
465    
466                    // if they've selected a resource, send to it
467                    if( sel != 0 && sel != 1 && sel != -1 )
468                    {
469                            to += "/" + (String)resourceBox.getSelectedItem();
470                    }
471    
472                    ArrayList send = new ArrayList();
473    
474                    if( sel != 1 || resourceBox.getItemCount() <= 2 )
475                    {
476                            send.add( to );
477                    }
478    
479                    // if they've selected to send to all resources, send to all
480                    else {
481                            Set keys = buddy.keySet();
482                            Iterator i = keys.iterator();
483                            while( i.hasNext() )
484                            {
485                                    String key = (String)i.next();
486                                    if( !key.equals( "N/A" ) ) send.add( buddy.getUser() + "/" + key );
487                            }
488                    }
489    
490                    for( int i = 0; i < send.size(); i++ )
491                    {
492                            Message message = new Message();
493    
494                            message.setBody( text );
495                            message.setType( Message.Type.CHAT );
496    
497                            message.setTo( (String)send.get( i ) );
498    
499                            if( BuddyList.getInstance().checkConnection() )
500                            {
501                                    BuddyList.getInstance().getConnection().sendPacket( message );
502                            }
503                            else {
504                                    BuddyList.getInstance().connectionError();
505                            }
506                    }
507            }
508    
509            /**
510             * Creates the containing frame
511            */
512            public void createFrame()
513            {
514                    frame = new JFrame();
515                    frame.setContentPane( this );
516                    frame.pack();
517    
518                    ImageIcon icon = StatusIconCache.getStatusIcon( org.jivesoftware.smack.packet.Presence.Mode.AVAILABLE );
519                    if( icon != null ) frame.setIconImage( icon.getImage() );
520    
521    
522                    frame.addWindowListener(
523                            new WindowAdapter()
524                            {
525                                    public void windowClosing( WindowEvent e )
526                                    {
527                                            if( Settings.getInstance().getProperty( "preserveMessages" ) == null )
528                                            {
529                                                    closeHandler();
530                                            }
531                                            else
532                                            {
533                                                    startTimer();
534                                                    frame.setVisible( false );
535                                            }
536                                    }
537                    } );
538    
539                    frame.setTitle( buddy.getName() + " (" + buddy.getUser() + ")" );
540                    frame.pack();
541    
542                    String stringWidth = Settings.getInstance().getProperty( "conversationWindowWidth" );
543                    String stringHeight = Settings.getInstance().getProperty( "conversationWindowHeight" );
544    
545                    if( stringWidth == null ) stringWidth = "400";
546                    if( stringHeight == null ) stringHeight = "340";
547    
548                    frame.setSize( new Dimension( Integer.parseInt( stringWidth ), Integer.parseInt( stringHeight ) ) );
549    
550                    // add a resize window listener
551                    frame.addComponentListener( new ComponentAdapter()
552                    {
553                            public void componentResized( ComponentEvent e )
554                            {
555                                    Dimension dim = frame.getSize();
556                                    Settings.getInstance().setProperty( "conversationWindowWidth", new Integer( (int)dim.getWidth() ).toString() );
557                                    Settings.getInstance().setProperty( "conversationWindowHeight", new Integer( (int)dim.getHeight() ).toString() );
558                            }
559                    } );
560                    Standard.cascadePlacement( frame );
561    
562                    setUpDivider();
563                    frame.setVisible( true );
564            }
565    
566            /**
567             * This renders the resource combo box - and displays icons for the online status
568             * of each resource
569             * @author Adam Olsen
570             * @version 1.0
571            */
572            class PresenceComboBoxRenderer extends JLabel implements ListCellRenderer
573            {
574                    public Component getListCellRendererComponent( JList list, Object value, int index,
575                            boolean isSelected, boolean cellHasFocus )
576                    {
577    
578                            Presence.Mode mode = null;
579    
580                            if( buddy.size() != 0 )
581                                    mode = buddy.getPresence( buddy.getHighestResource() );
582    
583                            if( value.toString().equals( resources.getString( "allResources" ) ) )
584                                    mode = Presence.Mode.AVAILABLE;
585    
586                            if( !value.toString().equals( resources.getString( "defaultResource" ) ) &&
587                                    !value.toString().equals( resources.getString( "allResources" ) ) )
588                                    mode = buddy.getPresence( value.toString() );
589    
590                            ImageIcon icon = StatusIconCache.getStatusIcon( mode );
591                            if ( icon != null ) setIcon( icon );
592                            setText( value.toString() );
593    
594                            return this;
595                    }
596            }
597    }