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    }