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 }