001 /* 002 * Copyright (C) 2003 Adam Olsen 003 * This program is free software; you can redistribute it and/or modify 004 * it under the terms of the GNU General Public License as published by 005 * the Free Software Foundation; either version 1, or (at your option) 006 * any later version. 007 * This program is distributed in the hope that it will be useful, 008 * but WITHOUT ANY WARRANTY; without even the implied warranty of 009 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 010 * GNU General Public License for more details. 011 * You should have received a copy of the GNU General Public License 012 * along with this program; if not, write to the Free Software 013 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 014 */ 015 package com.valhalla.jbother; 016 017 import java.awt.*; 018 import java.awt.event.*; 019 import java.io.*; 020 import java.util.*; 021 import com.valhalla.jbother.groupchat.*; 022 023 import javax.swing.*; 024 025 import org.jivesoftware.smack.XMPPConnection; 026 import org.jivesoftware.smack.packet.Presence; 027 028 import com.valhalla.gui.*; 029 import com.valhalla.jbother.*; 030 import com.valhalla.jbother.menus.BuddyListTopMenu; 031 import com.valhalla.jbother.jabber.*; 032 import com.valhalla.jbother.plugins.events.*; 033 import com.valhalla.pluginmanager.*; 034 import com.valhalla.settings.Settings; 035 import com.valhalla.jbother.preferences.PreferencesDialog; 036 037 /** 038 * BuddyList is the main controller for the buddy list, as as the buddy list is the main component 039 * of the IM application it performs most of the work once it's been initialized. 040 * 041 * @author Adam Olsen 042 * @created October 26, 2004 043 * @version 1.0 044 */ 045 public class BuddyList extends JFrame 046 { 047 private static BuddyList singleton = null; 048 private ResourceBundle resources = ResourceBundle.getBundle( "JBotherBundle", Locale.getDefault() ); 049 private XMPPConnection connection; 050 //the connection to the jabber server 051 private BuddyListTree buddyListTree; 052 //the inner tree - displays the buddies 053 private Container container; 054 055 //size and position 056 private int preferredWidth = 150; 057 private int preferredHeight = 300; 058 private double x = 100; 059 private double y = 100; 060 061 private boolean signoff = false; 062 063 private TabFrame tabFrame; 064 private BuddyListTopMenu topBuddyMenu = new BuddyListTopMenu( this ); 065 private Hashtable buddyStatuses = null; 066 private Presence.Mode currentMode = null; 067 private String currentStatusString = resources.getString( "available" ); 068 private AwayHandler awayHandler = new AwayHandler(); 069 private javax.swing.Timer awayTimer = new javax.swing.Timer( 600000, awayHandler ); 070 071 private boolean idleAway = false; 072 private Hashtable blockedUsers = new Hashtable(); 073 private static BuddyList buddyList = null; 074 075 /** 076 * Constructor for the buddy list. BuddyList is a singleton, so it's constructor 077 * is private 078 */ 079 private BuddyList() 080 { 081 082 super( "JBother" ); 083 setDefaultCloseOperation( DO_NOTHING_ON_CLOSE ); 084 085 ImageIcon icon = StatusIconCache.getStatusIcon( org.jivesoftware.smack.packet.Presence.Mode.AVAILABLE ); 086 if( icon != null ) 087 { 088 setIconImage( icon.getImage() ); 089 } 090 091 initComponents(); 092 //set up the visual components 093 setJMenuBar( topBuddyMenu ); 094 pack(); 095 096 DefaultFocusManager myManager = 097 new DefaultFocusManager() 098 { 099 public void processKeyEvent( Component focusedComponent, KeyEvent anEvent ) 100 { 101 // Returning when you receive CTRL-TAB makes your components able to control that key 102 if( anEvent.getKeyCode() == KeyEvent.VK_TAB && 103 ( anEvent.getModifiers() & Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() ) == Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() ) 104 { 105 106 if( anEvent.getID() == KeyEvent.KEY_RELEASED && tabFrame != null ) 107 { 108 tabFrame.switchTab(); 109 } 110 111 anEvent.consume(); 112 113 return; 114 } 115 116 super.processKeyEvent( focusedComponent, anEvent ); 117 } 118 }; 119 120 FocusManager.setCurrentManager( myManager ); 121 } 122 123 /** 124 * Gets the BuddyList singleton 125 * 126 * @return the BuddyList singleton 127 * @deprecated Use getInstance() instead 128 */ 129 public static BuddyList getSingleton() 130 { 131 return getInstance(); 132 } 133 134 /** 135 * Gets the BuddyList singleton 136 * 137 * @return the BuddyList singleton 138 */ 139 public static BuddyList getInstance() 140 { 141 if( singleton == null ) 142 { 143 singleton = new BuddyList(); 144 } 145 return singleton; 146 } 147 148 /** 149 * Returns the top buddy menu 150 * 151 * @return the buddy menu 152 */ 153 public BuddyListTopMenu getTopMenu() 154 { 155 return topBuddyMenu; 156 } 157 158 /** 159 * Starts the away timer 160 */ 161 public void startTimer() 162 { 163 // start the idle detection timer 164 if( Settings.getInstance().getBoolean( "autoAway" ) ) 165 { 166 com.valhalla.Logger.debug( "Starting away timer" ); 167 awayTimer.start(); 168 } 169 170 buddyListTree.setConnection( this.connection ); 171 AWTEventListener listener = new MyAWTEventListener(); 172 Toolkit.getDefaultToolkit().addAWTEventListener( listener, 173 AWTEvent.MOUSE_EVENT_MASK | AWTEvent.KEY_EVENT_MASK ); 174 } 175 176 /** 177 * Loads the blocked users information 178 */ 179 protected void loadBlockedUsers() 180 { 181 File file = new File( JBother.profileDir + File.separatorChar + "blocked" ); 182 String line = ""; 183 184 try 185 { 186 FileReader fr = new FileReader( file ); 187 BufferedReader in = new BufferedReader( fr ); 188 189 while( ( line = in.readLine() ) != null ) 190 { 191 blockedUsers.put( line, "blocked" ); 192 com.valhalla.Logger.debug( "Blocked> " + line ); 193 } 194 195 fr.close(); 196 } 197 catch( IOException e ) 198 { 199 com.valhalla.Logger.debug( "Blocked users file was not found or could not be read." ); 200 return; 201 } 202 } 203 204 /** 205 * Returns the away timer 206 * 207 * @return the away timer 208 */ 209 public javax.swing.Timer getAwayTimer() 210 { 211 return awayTimer; 212 } 213 214 public void setIdleAway( boolean idleAway ) { this.idleAway = idleAway; } 215 public boolean getIdleAway() { return idleAway; } 216 public AwayHandler getAwayHandler() { return awayHandler; } 217 218 /** 219 * Sets up the current connection 220 * 221 * @param connection the current connection 222 */ 223 public void init( XMPPConnection connection ) 224 { 225 this.connection = connection; 226 if( buddyStatuses == null ) buddyStatuses = new Hashtable(); 227 228 ConnectEvent event = new ConnectEvent( connection ); 229 PluginChain.fireEvent( event ); 230 231 buddyListTree.setConnection( connection ); 232 topBuddyMenu.getStatusMenu().setIcon( Presence.Mode.AVAILABLE ); 233 } 234 235 /** 236 * Clears the buddy tree 237 */ 238 public void clearTree() 239 { 240 buddyListTree.clearBuddies(); 241 } 242 243 /** 244 * initializes the buddy tree by loading the offline buddies 245 */ 246 public void initBuddies() 247 { 248 buddyListTree.loadOfflineBuddies(); 249 } 250 251 /** 252 * Sets the current presence mode 253 * 254 * @param mode the mode to set it to 255 */ 256 public void setCurrentPresenceMode( Presence.Mode mode ) 257 { 258 this.currentMode = mode; 259 } 260 261 /** 262 * Sets the current status string 263 * 264 * @param string the string to use 265 */ 266 public void setCurrentStatusString( String string ) 267 { 268 this.currentStatusString = string; 269 } 270 271 /** 272 * Gets the current presence mode 273 * 274 * @return the current presence mode 275 */ 276 public Presence.Mode getCurrentPresenceMode() 277 { 278 return this.currentMode; 279 } 280 281 /** 282 * Returns the current status string 283 * 284 * @return the current status string 285 */ 286 public String getCurrentStatusString() 287 { 288 return this.currentStatusString; 289 } 290 291 /** 292 * Returns the group chat frame 293 * 294 * @return the group chat frame 295 */ 296 public TabFrame getTabFrame() 297 { 298 return tabFrame; 299 } 300 301 /** 302 * Starts the group chat frame 303 */ 304 public void startTabFrame() 305 { 306 if( tabFrame == null ) 307 { 308 tabFrame = new TabFrame(); 309 } 310 } 311 312 /** 313 * checks to see if there are no more chat room windows in the groupchatframe 314 * if there are no more, the groupchatframe is destroyed 315 */ 316 public void stopTabFrame() 317 { 318 if( tabFrame == null ) 319 { 320 return; 321 } 322 if( tabFrame.tabsLeft() <= 0 ) 323 { 324 Point location = new Point( tabFrame.getLocationOnScreen() ); 325 Settings.getInstance().setProperty( "tabFrameX", new Double( location.getX() ).toString() ); 326 Settings.getInstance().setProperty( "tabFrameY", new Double( location.getY() ).toString() ); 327 328 DialogTracker.removeDialog( tabFrame ); 329 tabFrame = null; 330 com.valhalla.Logger.debug( "Removing chat frame" ); 331 } 332 } 333 334 /** 335 * Removes a ChatPanel 336 * 337 * @param panel the chat panel to remove 338 */ 339 public void removeTabPanel( TabFramePanel panel ) 340 { 341 if( tabFrame == null ) 342 { 343 return; 344 } 345 346 tabFrame.removePanel( panel ); 347 stopTabFrame(); 348 } 349 350 /** 351 * Adds a chat room window to the groupchat frame. 352 * If there is not groupchat frame one is created 353 * 354 * @param panel The feature to be added to the TabPanel attribute 355 */ 356 public void addTabPanel( TabFramePanel panel ) 357 { 358 if( tabFrame == null ) 359 { 360 tabFrame = new TabFrame(); 361 } 362 tabFrame.addPanel( panel ); 363 364 if( !tabFrame.isVisible() ) 365 { 366 tabFrame.setVisible( true ); 367 tabFrame.toFront(); 368 } 369 370 panel.focusYourself(); 371 372 tabFrame.setStatus( getCurrentPresenceMode(), getCurrentStatusString() ); 373 } 374 375 /** 376 * Gets the buddy status 377 * 378 * @param userId the user id of the BuddyStatus 379 * @return The buddyStatus value 380 */ 381 public BuddyStatus getBuddyStatus( String userId ) 382 { 383 if( buddyStatuses == null ) 384 { 385 buddyStatuses = new Hashtable(); 386 } 387 BuddyStatus buddy = new BuddyStatus( userId ); 388 389 if( buddyStatuses.get( userId.toLowerCase() ) != null ) 390 { 391 buddy = (BuddyStatus)buddyStatuses.get( userId.toLowerCase() ); 392 } 393 else 394 { 395 buddyStatuses.put( userId.toLowerCase(), buddy ); 396 } 397 398 return buddy; 399 } 400 401 /** 402 * Returns all the buddy status that are available 403 * 404 * @return the Hashtable containing all the buddy statuses 405 */ 406 public Hashtable getBuddyStatuses() 407 { 408 return this.buddyStatuses; 409 } 410 411 /** 412 * Returns the current connection 413 * 414 * @return the current connection 415 */ 416 public XMPPConnection getConnection() 417 { 418 return this.connection; 419 } 420 421 /** 422 * Checks to see if a connection is active 423 * @return true if the connection is active (connected) 424 */ 425 public boolean checkConnection() 426 { 427 if( connection == null || !connection.isConnected() ) return false; 428 else return true; 429 } 430 431 /** 432 * Displays a generic connection error 433 */ 434 public void connectionError() 435 { 436 Standard.warningMessage( null, resources.getString( "connectionError" ), resources.getString( "notConnected" ) ); 437 } 438 439 /** 440 * Loads the saved settings from any previous settings 441 */ 442 protected void setPreferredLocation() 443 { 444 //load the settings from the settings file 445 String xString = Settings.getInstance().getProperty( "buddyListX" ); 446 String yString = Settings.getInstance().getProperty( "buddyListY" ); 447 448 if( yString == null ) yString = "100"; 449 if( xString == null ) xString = "100"; 450 451 double x = Double.parseDouble( xString ); 452 double y = Double.parseDouble( yString ); 453 454 if( x < 0.0 ) x = 100.0; 455 if( y < 0.0 ) y = 100.0; 456 457 setLocation( (int)x, (int)y ); 458 } 459 460 /** 461 * Saves the current settings - like the height and width of the buddy list 462 */ 463 public void saveSettings() 464 { 465 if( isVisible() ) 466 { 467 //save the current buddy list size and position 468 Dimension size = new Dimension( getSize() ); 469 Point location = new Point( getLocationOnScreen() ); 470 Settings.getInstance().setProperty( "buddyListX", new Double( location.getX() ).toString() ); 471 Settings.getInstance().setProperty( "buddyListY", new Double( location.getY() ).toString() ); 472 Settings.getInstance().setProperty( "buddyListWidth", new Integer( size.width ).toString() ); 473 Settings.getInstance().setProperty( "buddyListHeight", new Integer( size.height ).toString() ); 474 } 475 476 //the "show" state settings 477 Settings.getInstance().setBoolean( "showOfflineBuddies", buddyListTree.getShowOfflineBuddies() ); 478 Settings.getInstance().setBoolean( "showUnfiledBuddies", buddyListTree.getShowUnfiledBuddies() ); 479 Settings.getInstance().setBoolean( "showAgentBuddies", buddyListTree.getShowAgentBuddies() ); 480 Settings.getInstance().setBoolean( "showAgentMessages", buddyListTree.getShowAgentMessages() ); 481 Settings.getInstance().writeSettings(); 482 } 483 484 /** 485 * Load saved settings from the last session and set the buddy list to the 486 * sizes that were saved. 487 */ 488 protected void setPreferredDimensions() 489 { 490 String width = Settings.getInstance().getProperty( "buddyListWidth" ); 491 String height = Settings.getInstance().getProperty( "buddyListHeight" ); 492 493 Dimension dim = new Dimension( 100, 100 ); 494 495 try { 496 dim.setSize( Integer.parseInt( width ), Integer.parseInt( height ) ); 497 } 498 catch( NumberFormatException ex ) { } 499 500 setSize( dim ); 501 } 502 503 /** 504 * Returns the buddy list tree 505 * 506 * @return the buddy list tree 507 */ 508 public BuddyListTree getBuddyListTree() 509 { 510 return this.buddyListTree; 511 } 512 513 /** 514 * Init components just does some initial setup, sets the size and preferred location 515 * of the buddy list. Sets up an new initial tree. 516 */ 517 private void initComponents() 518 { 519 container = getContentPane(); 520 container.setLayout( new BorderLayout() ); 521 522 buddyListTree = new BuddyListTree(); 523 524 container.add( buddyListTree, BorderLayout.CENTER ); 525 addWindowListener( 526 new WindowAdapter() 527 { 528 public void windowClosing( WindowEvent e ) 529 { 530 ExitingEvent event = new ExitingEvent( getInstance() ); 531 PluginChain.fireEvent( event ); 532 if( event.getExit() ) quitHandler(); 533 } 534 535 } ); 536 } 537 538 /** 539 * Sets whether or not we have signed off 540 * 541 * @param value true if we have signed off 542 */ 543 public void setSignoff( boolean value ) 544 { 545 this.signoff = value; 546 } 547 548 /** 549 * Gets whether or not we have signed off 550 * 551 * @return true if we have signed off 552 */ 553 public boolean getSignoff() 554 { 555 return signoff; 556 } 557 558 /** 559 * Gets the displayed name, or "me" 560 * 561 * @return the myDisplayedName setting 562 */ 563 public String getMyName() 564 { 565 if( Settings.getInstance().getProperty( "myDisplayedName" ) != null ) 566 { 567 return Settings.getInstance().getProperty( "myDisplayedName" ); 568 } 569 return connection.getUser(); 570 } 571 572 /** 573 * Listens for user input events. If JBother is set to go "Away" on idle, this will restart 574 * the idle timer. 575 * 576 * @author Adam Olsen 577 * @created October 26, 2004 578 * @version 1.0 579 */ 580 public class MyAWTEventListener implements AWTEventListener 581 { 582 /** 583 * Called by the event listener 584 * 585 * @param evt the event 586 */ 587 public void eventDispatched( AWTEvent evt ) 588 { 589 if( awayTimer.isRunning() ) 590 { 591 awayTimer.restart(); 592 } 593 else if( idleAway ) 594 { 595 setStatus( Presence.Mode.AVAILABLE, getCurrentStatusString(), false ); 596 awayTimer.start(); 597 idleAway = false; 598 } 599 } 600 } 601 602 /** 603 * Closes the application 604 * 605 */ 606 public void quitHandler() 607 { 608 saveSettings(); 609 if( connection != null && connection.isConnected() ) connection.close(); 610 //and finally close the connection 611 com.valhalla.Logger.closeLog(); 612 System.exit( 0 ); 613 } 614 615 /** 616 * signs off, clears the buddy list 617 */ 618 public void signOff() 619 { 620 signoff = false; 621 buddyListTree.clearBuddies(); 622 topBuddyMenu.getStatusMenu().setIcon( (Presence.Mode)null ); 623 connection.close(); 624 connection = null; 625 626 if( buddyStatuses != null ) 627 { 628 Iterator iterator = buddyStatuses.keySet().iterator(); 629 while( iterator.hasNext() ) 630 { 631 String user = (String)iterator.next(); 632 BuddyStatus buddy = (BuddyStatus)buddyStatuses.get( user ); 633 buddy.resetBuddy(); 634 if( buddy.getConversation() != null ) 635 { 636 ((ChatPanel)buddy.getConversation()).disconnected(); 637 } 638 } 639 } 640 641 if( tabFrame != null ) 642 { 643 ArrayList remove = new ArrayList(); 644 645 JTabbedPane pane = tabFrame.getTabPane(); 646 for( int i = 0; i < pane.getTabCount(); i++ ) 647 { 648 TabFramePanel panel = (TabFramePanel)pane.getComponentAt( i ); 649 if( panel instanceof ChatRoomPanel ) 650 { 651 remove.add( panel ); 652 } 653 } 654 655 for( int i = 0; i < remove.size(); i++ ) 656 { 657 TabFramePanel panel = (TabFramePanel)remove.get( i ); 658 removeTabPanel( panel ); 659 ((ChatRoomPanel)panel).leave(); 660 } 661 } 662 } 663 664 665 /** 666 * Close down the buddy list. 667 * Also closes down any other windows that might 668 * be open 669 */ 670 public void kill() 671 { 672 if( tabFrame != null ) 673 { 674 tabFrame.leaveAll(); 675 } 676 677 saveSettings(); 678 679 setVisible( false ); 680 681 currentMode = Presence.Mode.AVAILABLE; 682 currentStatusString = resources.getString( "available" ); 683 connection = null; 684 685 awayTimer.stop(); 686 687 Vector panels = MessageDelegator.getInstance().getPanels(); 688 int size = panels.size(); 689 for( int i = 0; i < size; i++ ) 690 { 691 ConversationPanel panel = (ConversationPanel)panels.get( 0 ); 692 panel.closeHandler(); 693 } 694 695 panels.removeAllElements(); 696 697 DialogTracker.kill(); 698 signoff = false; 699 } 700 701 /** 702 * Gets a list of blocked users 703 * 704 * @return the list of blocked users 705 */ 706 public Hashtable getBlockedUsers() 707 { 708 return blockedUsers; 709 } 710 711 /** 712 * Sets the list of blocked users 713 * 714 * @param users the list of blocked users 715 */ 716 public void setBlockedUsers( Hashtable users ) 717 { 718 this.blockedUsers = users; 719 } 720 721 /** 722 * Updates all the dialogs window icons 723 */ 724 public void updateIcons() 725 { 726 StatusIconCache.clearStatusIconCache(); 727 ImageIcon icon = StatusIconCache.getStatusIcon( org.jivesoftware.smack.packet.Presence.Mode.AVAILABLE ); 728 if( icon != null ) 729 { 730 setIconImage( icon.getImage() ); 731 topBuddyMenu.getStatusMenu().setIcon( icon ); 732 733 DialogTracker tracker = DialogTracker.getInstance(); 734 for( int i = 0; i < tracker.size(); i++ ) 735 { 736 if( tracker.get( i ) instanceof JFrame ) 737 { 738 ( (JFrame)tracker.get( i ) ).setIconImage( icon.getImage() ); 739 } 740 } 741 } 742 topBuddyMenu.getStatusMenu().reloadStatusIcons(); 743 } 744 745 /** 746 * Set the current status by sending a Jabber packet 747 * 748 * @param mode the mode to set it to 749 * @param defaultMessage the status message to set it to 750 * @param getMessage whether or not to get a new message 751 * @return true if the status packet was sent successfully 752 */ 753 public boolean setStatus( Presence.Mode mode, String defaultMessage, boolean getMessage ) 754 { 755 String result; 756 757 if( getMessage ) 758 { 759 result = (String)JOptionPane.showInputDialog( null, 760 resources.getString( "enterStatusMessage" ), 761 resources.getString( "setStatus" ), JOptionPane.QUESTION_MESSAGE, null, null, defaultMessage ); 762 } 763 else 764 { 765 result = defaultMessage; 766 } 767 768 if( result == null || result.equals( "" ) ) 769 { 770 return false; 771 } 772 else 773 { 774 775 if( Settings.getInstance().getBoolean( "autoAway" ) ) 776 { 777 if( mode == Presence.Mode.AVAILABLE ) 778 { 779 awayTimer.start(); 780 } 781 else 782 { 783 awayTimer.stop(); 784 } 785 } 786 787 int priority = 5; 788 try 789 { 790 priority = Integer.parseInt( Settings.getInstance().getProperty( "priority" ) ); 791 } 792 catch( NumberFormatException nfe ) 793 { 794 } 795 796 if( !checkConnection() && !idleAway ) 797 { 798 Thread thread = new Thread( new ConnectorThread( mode, result, false ) ); 799 thread.start(); 800 return true; 801 } 802 803 Presence presence = new Presence( Presence.Type.AVAILABLE, result, priority, mode ); 804 805 connection.sendPacket( presence ); 806 807 setCurrentPresenceMode( mode ); 808 if( !idleAway ) 809 { 810 setCurrentStatusString( result ); 811 } 812 813 // if the group chat window is open, set the status there too 814 if( getTabFrame() != null ) 815 { 816 getTabFrame().setStatus( mode, result ); 817 } 818 819 ParsedBuddyInfo info = new ParsedBuddyInfo( connection.getUser() ); 820 BuddyStatus buddy = BuddyList.getInstance().getBuddyStatus( info.getUserId() ); 821 buddy.addResource( info.getResource(), priority, mode, result ); 822 topBuddyMenu.getStatusMenu().loadSelfStatuses(); 823 824 updateIcons(); 825 } 826 827 return true; 828 } 829 } 830 831 /** 832 * Sets the user to away after a specific amount of idle time 833 * 834 * @author Adam Olsen 835 * @created October 26, 2004 836 * @version 1.0 837 */ 838 class AwayHandler implements ActionListener 839 { 840 private ResourceBundle resources = ResourceBundle.getBundle( "JBotherBundle", Locale.getDefault() ); 841 842 /** 843 * Called by the enclosing event listener 844 * 845 * @param e the event 846 */ 847 public void actionPerformed( ActionEvent e ) 848 { 849 BuddyList.getInstance().setIdleAway( true ); 850 BuddyList.getInstance().setStatus( Presence.Mode.AWAY, resources.getString( "autoAway" ), false ); 851 BuddyList.getInstance().getAwayTimer().stop(); 852 } 853 }