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 //includes 022 import java.awt.event.*; 023 import java.text.*; 024 025 import javax.swing.*; 026 import java.awt.*; 027 028 import javax.swing.event.*; 029 import java.util.*; 030 031 import javax.swing.event.TreeExpansionEvent; 032 import javax.swing.event.TreeExpansionListener; 033 import javax.swing.event.TreeModelEvent; 034 import javax.swing.event.TreeModelListener; 035 import javax.swing.tree.*; 036 037 import org.jivesoftware.smack.Roster; 038 import org.jivesoftware.smack.RosterEntry; 039 import org.jivesoftware.smack.RosterGroup; 040 import org.jivesoftware.smack.XMPPConnection; 041 042 import com.valhalla.jbother.*; 043 import com.valhalla.jbother.menus.BuddyListPopupMenu; 044 import com.valhalla.jbother.jabber.BuddyStatus; 045 import com.valhalla.settings.Settings; 046 import org.dotuseful.ui.tree.*; 047 048 /** 049 * BuddyListTree is the part of the buddy list dialog that draws the buddies and their 050 * groups from your Jabber roster. It also displays different pictures for different statuses. 051 * 052 * @author Adam Olsen 053 * @version 1.0 054 **/ 055 public class BuddyListTree extends JPanel 056 { 057 private ResourceBundle resources = ResourceBundle.getBundle( "JBotherBundle", Locale.getDefault() ); 058 private XMPPConnection connection; 059 private Roster roster; 060 061 private AutomatedTreeNode root = new AutomatedTreeNode( "Buddies" ); 062 private AutomatedTreeModel model = new AutomatedTreeModel( root ); 063 private JTree tree = new JTree( model ); 064 065 private JScrollPane scrollPane = new JScrollPane( tree ); 066 private BuddyListPopupMenu buddyPopupMenu = new BuddyListPopupMenu(); 067 private boolean showOfflineBuddies = Settings.getInstance().getBoolean( "showOfflineBuddies" ); 068 private boolean showUnfiledBuddies = Settings.getInstance().getBoolean( "showUnfiledBuddies" ); 069 private boolean showAgentBuddies = Settings.getInstance().getBoolean( "showAgentBuddies" ); 070 private boolean showAgentMessages = Settings.getInstance().getBoolean( "showAgentMessages" ); 071 private BuddyListRenderer renderer = new BuddyListRenderer(); 072 private BuddyListExpansionListener expandListener = new BuddyListExpansionListener(); 073 private TreeMap buddyGroups = new TreeMap(); // to sort the buddy groups 074 075 /** 076 * Sets up the tree 077 */ 078 public BuddyListTree() 079 { 080 setLayout( new GridLayout( 0, 1 ) ); 081 setBackground( Color.WHITE ); 082 083 tree.setCellRenderer( renderer ); 084 tree.setRootVisible( false ); 085 tree.setRowHeight( 0 ); 086 tree.setShowsRootHandles( true ); 087 tree.getSelectionModel().setSelectionMode( TreeSelectionModel.SINGLE_TREE_SELECTION ); 088 tree.addMouseListener( new PopupMouseListener() ); 089 tree.addTreeExpansionListener( expandListener ); 090 091 ToolTipManager.sharedInstance().registerComponent( tree ); 092 add( scrollPane ); 093 } 094 095 /** 096 * Returns the JTree 097 * @return the actual JTree swing component 098 */ 099 public JTree getTree() { return this.tree; } 100 101 /** 102 * Sets the JTree's XMPPConnection 103 * @param connection the current connection 104 */ 105 public void setConnection( XMPPConnection connection ) 106 { 107 this.connection = connection; 108 if( connection == null ) return; 109 this.roster = connection.getRoster(); 110 } 111 112 /** 113 * Sets whether or not to show the offline buddies 114 * @param show true to show the offline buddies 115 */ 116 public void setShowOfflineBuddies( boolean show ) 117 { 118 this.showOfflineBuddies = show; 119 reloadBuddies(); 120 } 121 122 /** 123 * Sets whether or not to show the unfiled buddies 124 * @param show true to show unfiled buddies 125 */ 126 public void setShowUnfiledBuddies( boolean show ) 127 { 128 this.showUnfiledBuddies = show; 129 reloadBuddies(); 130 } 131 132 /** 133 * Sets whether or not to show agents/transports 134 * @param show true to show agents and transports 135 */ 136 public void setShowAgentBuddies( boolean show ) 137 { 138 this.showAgentBuddies = show; 139 reloadBuddies(); 140 } 141 142 /** 143 * Set to true if you want to show messages from agents 144 * @param show whether or not to recieve messages 145 */ 146 public void setShowAgentMessages( boolean show ) 147 { 148 this.showAgentMessages = show; 149 } 150 151 /** 152 * Returns whether or not offlines buddies are showing 153 * @return true if offline buddies are showing 154 */ 155 public boolean getShowOfflineBuddies() { return this.showOfflineBuddies; } 156 157 /** 158 * Returns whether or not unfiled buddies are showing 159 * @return true if unfiled buddies are showing 160 */ 161 public boolean getShowUnfiledBuddies() { return this.showUnfiledBuddies; } 162 163 /** 164 * Returns whether or not agent buddies are showing 165 * @return true if agent buddies are showing 166 */ 167 public boolean getShowAgentBuddies() { return this.showAgentBuddies; } 168 169 /** 170 * Returns whether or now agent messages are to be recieved 171 * @return true if agent messages are being recieved 172 */ 173 public boolean getShowAgentMessages() { return this.showAgentMessages; } 174 175 /** 176 * To listen to when a group gets expanded. Saves it in the settings 177 * for session restoration 178 * @author Adam Olsen 179 * @version 1.0 180 */ 181 class BuddyListExpansionListener implements TreeExpansionListener 182 { 183 /** 184 * Listens for a tree collapse event 185 */ 186 public void treeCollapsed( TreeExpansionEvent e ) 187 { 188 TreePath path = e.getPath(); 189 AutomatedTreeNode node = (AutomatedTreeNode)path.getLastPathComponent(); 190 191 Settings.getInstance().setProperty( "groupExpandStatus_" + node.getUserObject().toString(), "collapsed" ); 192 } 193 194 /** 195 * Listens for a tree expand event 196 */ 197 public void treeExpanded( TreeExpansionEvent e ) 198 { 199 TreePath path = e.getPath(); 200 AutomatedTreeNode node = (AutomatedTreeNode)path.getLastPathComponent(); 201 202 Settings.getInstance().remove( "groupExpandStatus_" + node.getUserObject().toString() ); 203 } 204 } 205 206 /** 207 * Redraws the JTree 208 */ 209 public void reloadBuddies() 210 { 211 reloadBuddies( false ); 212 } 213 214 /** 215 * Shows all the current offline buddies 216 */ 217 public void loadOfflineBuddies() 218 { 219 reloadBuddies( true ); 220 } 221 222 /** 223 * Redraws the JTree 224 * @param loadOffline whether or not to just load the offline buddies 225 */ 226 public void reloadBuddies( final boolean loadOffline ) 227 { 228 SwingUtilities.invokeLater( new Runnable() 229 { 230 public void run() 231 { 232 if( connection == null ) return; 233 if( roster == null ) roster = connection.getRoster(); 234 235 if( !loadOffline ) clearBuddies(); 236 237 // loop through all the RosterEntries and see if they need to be added to the 238 // BuddyList tree 239 Iterator it = roster.getEntries(); 240 while( it.hasNext() ) 241 { 242 RosterEntry entry = (RosterEntry)it.next(); 243 BuddyStatus buddy = BuddyList.getInstance().getBuddyStatus( entry.getUser() ); 244 245 if( loadOffline && buddy.size() <= 0 ) checkAddEntry( buddy ); 246 else checkAddEntry( buddy ); 247 } 248 249 tree.repaint(); 250 } 251 } ); 252 } 253 254 /** 255 * Clears all the buddies from the JTree 256 */ 257 public void clearBuddies() 258 { 259 com.valhalla.Logger.debug( "Clearing Buddies" ); 260 // clear the JTree and the BuddyGroups TreeMap 261 root.removeAllChildren(); 262 buddyGroups.clear(); 263 } 264 265 /** 266 * We store all of the Group names in a TreeMap so that we can get the sort 267 * index when inserting the TreeNode for this group into the root node 268 * @param group the name of the group 269 **/ 270 private int getGroupIndex( String group ) 271 { 272 synchronized( buddyGroups ) 273 { 274 if( buddyGroups.containsKey( group ) ) return -1; 275 // we want General Contacts and Agents/Transports to always 276 // sort at the bottom 277 if( group.equals( resources.getString( "contactsGroup" ) ) ) group = "zzz Contacts"; 278 else if( group.equals( resources.getString( "transportsGroup" ) ) ) group = "zzzz Agents/Transports"; 279 280 buddyGroups.put( group, new TreeMap() ); 281 282 int count = 0; 283 284 // find the index of the newly sorted group 285 Iterator i = buddyGroups.keySet().iterator(); 286 while( i.hasNext() ) 287 { 288 String key = (String)i.next(); 289 if( key.equals( group ) ) break; 290 count++; 291 } 292 293 return count; 294 } 295 } 296 297 /** 298 * Finds out the sort index of this particular buddy 299 * @param group the group the buddy is in 300 * @param buddy the buddy to find the index of 301 **/ 302 private int getBuddyIndex( String group, BuddyStatus buddy ) 303 { 304 synchronized( buddyGroups ) 305 { 306 if( group.equals( resources.getString( "contactsGroup" ) ) ) group = "zzz Contacts"; 307 else if( group.equals( resources.getString( "transportsGroup" ) ) ) group = "zzzz Agents/Transports"; 308 309 String name = buddy.getName(); 310 if( name == null ) name = buddy.getUser(); 311 name = name.toLowerCase(); 312 313 if( buddyGroups.get( group ) == null ) buddyGroups.put( group, new TreeMap() ); 314 315 ((TreeMap)buddyGroups.get( group )).put( name, buddy.getUser() ); 316 317 int count = 0; 318 319 Iterator i = ((TreeMap)buddyGroups.get( group )).keySet().iterator(); 320 while( i.hasNext() ) 321 { 322 String key = (String)i.next(); 323 if( key.equals( name ) ) break; 324 count++; 325 } 326 327 return count; 328 } 329 } 330 331 /** 332 * Checks to see if the group is already in the tree, and returns the index of it 333 * @param group the group to check 334 **/ 335 public AutomatedTreeNode checkGroup( final String group ) 336 { 337 boolean check = false; 338 AutomatedTreeNode node = new AutomatedTreeNode( group ); 339 340 Enumeration children = root.children(); 341 342 // find out if the group alread exists 343 whileLoop: 344 while( children.hasMoreElements() ) 345 { 346 AutomatedTreeNode theNode = (AutomatedTreeNode)children.nextElement(); 347 String g = (String)theNode.getUserObject(); 348 if( g.equals( group ) ) 349 { 350 node = theNode; 351 check = true; 352 break whileLoop; 353 } 354 } 355 356 if( root.isNodeChild( node ) ) return node; 357 358 final String tempGroup = group; 359 final AutomatedTreeNode tempNode = node; 360 361 if( !check ) 362 { 363 int num = getGroupIndex( tempGroup ); 364 if( num >= 0 ) root.insert( tempNode, num ); 365 } 366 367 return node; 368 369 370 } 371 372 /** 373 * finds out if the buddy should be displayed in the BuddyListTree. 374 * If so the buddy is added to the tree 375 * @param buddy the buddy to add 376 **/ 377 public void checkAddEntry( final BuddyStatus buddy ) 378 { 379 if( buddy == null ) return; 380 381 boolean add = false; 382 383 // if we are set to show the offline buddies then add the buddy to the tree 384 if( showOfflineBuddies ) add = true; 385 else { 386 // otherwise we have to find out if the buddy is online before we add it 387 if( buddy.size() > 0 ) add = true; 388 } 389 390 if( buddy.getRemoved() ) add = false; 391 392 if( add ) 393 { 394 // find the group that the buddy belongs to 395 396 String tempGroup = buddy.getTempGroup(); 397 if( tempGroup == null ) tempGroup = buddy.getGroup(); 398 399 final String group = tempGroup; 400 401 if( !showUnfiledBuddies && group.equals( resources.getString( "contactsGroup" ) ) ) return; 402 if( !showAgentBuddies && group.equals( resources.getString( "transportsGroup" ) ) ) return; 403 404 final AutomatedTreeNode node = checkGroup( group ); 405 int index = getBuddyIndex( group, buddy ); 406 407 if( !isInTree( buddy ) ) node.insert( new AutomatedTreeNode( buddy ), 408 index ); 409 410 TreePath parent = new TreePath( root ); 411 tree.expandPath( parent ); 412 413 // find out if we need to expand this group 414 String property = Settings.getInstance().getProperty( "groupExpandStatus_" + group ); 415 if( property == null || !property.equals( "collapsed" ) ) 416 { 417 tree.expandPath( parent.pathByAddingChild( node ) ); 418 } 419 } 420 421 } 422 423 /** 424 * Returns whether or not the buddy is in the tree 425 * @param buddy the buddy to check 426 * @return true if the buddy is in the tree 427 */ 428 public boolean isInTree( BuddyStatus buddy ) 429 { 430 String group = buddy.getGroup(); 431 432 // loop through all the groups until we find the group that this 433 // buddy belongs to 434 Enumeration children = root.children(); 435 while( children.hasMoreElements() ) 436 { 437 AutomatedTreeNode node = (AutomatedTreeNode)children.nextElement(); 438 439 // once we find it's group, loop through all the buddies in that group 440 if( node.getUserObject().toString().equals( group ) ) 441 { 442 Enumeration leafs = node.children(); 443 while( leafs.hasMoreElements() ) 444 { 445 AutomatedTreeNode leaf = (AutomatedTreeNode)leafs.nextElement(); 446 if( leaf.getUserObject() == buddy ) return true; 447 } 448 } 449 } 450 451 return false; 452 } 453 454 /** 455 * Adds a buddy to the tree - if it's not already in the tree 456 * @param buddy the buddy to add 457 */ 458 public void addBuddy( final BuddyStatus buddy ) 459 { 460 SwingUtilities.invokeLater( new Runnable() 461 { 462 public void run() 463 { 464 checkAddEntry( buddy ); 465 tree.repaint(); 466 validate(); 467 repaint(); 468 } 469 } ); 470 } 471 472 /** 473 * Removes the buddy from the tree 474 * @param buddy the buddy to remove 475 * @param group the group the buddy is in 476 */ 477 public void removeBuddy( final BuddyStatus buddy, final String group ) 478 { 479 SwingUtilities.invokeLater( new Runnable() 480 { 481 public void run() 482 { 483 // loop through all the groups until we find the group that this 484 // buddy belongs to 485 Enumeration children = root.children(); 486 while( children.hasMoreElements() ) 487 { 488 AutomatedTreeNode node = (AutomatedTreeNode)children.nextElement(); 489 490 // once we find it's group, loop through all the buddies in that group 491 if( node.getUserObject().toString().equals( group ) ) 492 { 493 Enumeration leafs = node.children(); 494 while( leafs.hasMoreElements() ) 495 { 496 AutomatedTreeNode leaf = (AutomatedTreeNode)leafs.nextElement(); 497 Object check = leaf.getUserObject(); 498 if( check instanceof BuddyStatus ) 499 { 500 BuddyStatus temp = (BuddyStatus)check; 501 502 if( temp.getUser().equals( buddy.getUser() ) ) 503 { 504 // once we find the buddy, remove it 505 removeBuddyNode( node, leaf ); 506 } 507 } 508 } 509 } 510 } 511 } 512 } ); 513 } 514 515 /** 516 * removes this buddy from the JTree. If this was the last buddy in the group 517 * then remove the group from being displayed 518 * @param node the group node 519 * @param leaf the leaf node 520 **/ 521 private void removeBuddyNode( final AutomatedTreeNode node, final AutomatedTreeNode leaf ) 522 { 523 String group = (String)node.getUserObject(); 524 525 if( group.equals( resources.getString( "contactsGroup" ) ) ) group = "zzz Contacts"; 526 else if( group.equals( resources.getString( "transportsGroup" ) ) ) group = "zzzz Agents/Transports"; 527 528 BuddyStatus buddy = (BuddyStatus)leaf.getUserObject(); 529 530 String name = buddy.getName(); 531 if( name == null ) name = buddy.getUser(); 532 String jid = buddy.getUser(); 533 534 synchronized (buddyGroups) { 535 TreeMap buddies = ((TreeMap) buddyGroups.get(group)); 536 Iterator i = buddies.keySet().iterator(); 537 while (i.hasNext()) { 538 String key = (String) i.next(); 539 if (buddies.get(key).equals(jid)) { 540 ((TreeMap) buddyGroups.get(group)).remove(key); 541 break; 542 } 543 } 544 545 } 546 547 node.remove( leaf ); 548 if( node.getChildCount() <= 0 ) 549 { 550 buddyGroups.remove( group ); 551 root.remove( node ); 552 } 553 554 tree.repaint(); 555 } 556 557 /** 558 * Starts a conversation if someone double double clicks on a buddy 559 */ 560 public void initiateConversation() 561 { 562 if( tree.getSelectionPath() == null ) return; 563 TreePath path = tree.getSelectionPath(); 564 AutomatedTreeNode node = (AutomatedTreeNode)path.getLastPathComponent(); 565 if( model.isLeaf( node ) ) 566 { 567 BuddyStatus buddy = (BuddyStatus)node.getUserObject(); 568 569 if( buddy.getConversation() == null ) 570 { 571 ChatPanel conver = new ChatPanel( buddy ); 572 buddy.setConversation( conver ); 573 MessageDelegator.getInstance().showPanel( buddy.getConversation() ); 574 MessageDelegator.getInstance().frontFrame( buddy.getConversation() ); 575 } 576 else { 577 MessageDelegator.getInstance().showPanel( buddy.getConversation() ); 578 MessageDelegator.getInstance().frontFrame( buddy.getConversation() ); 579 } 580 581 buddy.getConversation().stopTimer(); 582 } 583 } 584 585 /** 586 * Listens for mouse events in the tree 587 * @author Adam Olsen 588 * @version 1.0 589 */ 590 class PopupMouseListener extends MouseAdapter 591 { 592 public void mousePressed( MouseEvent e ) { checkPop( e ); } 593 public void mouseReleased( MouseEvent e ) { checkPop( e ); } 594 public void mouseClicked( MouseEvent e ) 595 { 596 checkPop( e ); 597 try { 598 JTree tree = (JTree)e.getComponent(); 599 } 600 catch( ClassCastException ex ) { return; } 601 602 if( e.getClickCount() >= 2 ) 603 { 604 initiateConversation(); 605 } 606 } 607 608 /** 609 * Checks if we need to display the buddy list popup menu 610 * Shows it if needs be 611 */ 612 public void checkPop( MouseEvent e ) 613 { 614 BuddyStatus buddy = null; 615 616 if( e.isPopupTrigger() ) 617 { 618 try { 619 620 JTree tree = (JTree)e.getComponent(); 621 622 TreePath path = tree.getPathForLocation( e.getX(), e.getY() ); 623 if( path == null ) throw new ClassCastException(); 624 625 tree.setSelectionPath( path ); 626 627 AutomatedTreeNode node = (AutomatedTreeNode)path.getLastPathComponent(); 628 buddy = (BuddyStatus)node.getUserObject(); 629 buddyPopupMenu.showMenu( e.getComponent(), e.getX(), e.getY(), buddy ); 630 } 631 catch( ClassCastException ex ) { /*is not a buddy, so don't display the menu*/ } 632 } 633 } 634 } 635 }