001/*
002 * Copyright 2010-2015 Institut Pasteur.
003 * 
004 * This file is part of Icy.
005 * 
006 * Icy is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 * 
011 * Icy is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014 * GNU General Public License for more details.
015 * 
016 * You should have received a copy of the GNU General Public License
017 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
018 */
019package icy.gui.inspector;
020
021import icy.gui.component.CloseableTabbedPane;
022import icy.gui.component.CloseableTabbedPane.CloseableTabbedPaneListener;
023import icy.gui.component.ExternalizablePanel;
024import icy.gui.component.IcyTextField;
025import icy.gui.component.button.IcyButton;
026import icy.gui.component.button.IcyToggleButton;
027import icy.gui.frame.progress.AnnounceFrame;
028import icy.gui.frame.progress.ToolTipFrame;
029import icy.gui.main.IcyDesktopPane;
030import icy.gui.main.IcyDesktopPane.AbstractDesktopOverlay;
031import icy.gui.main.MainFrame;
032import icy.gui.preferences.ChatPreferencePanel;
033import icy.gui.preferences.PreferenceFrame;
034import icy.gui.util.ComponentUtil;
035import icy.gui.util.FontUtil;
036import icy.gui.util.GuiUtil;
037import icy.main.Icy;
038import icy.network.IRCClient;
039import icy.network.IRCEventListenerImpl;
040import icy.network.IRCUtil;
041import icy.network.NetworkUtil;
042import icy.network.NetworkUtil.InternetAccessListener;
043import icy.preferences.ChatPreferences;
044import icy.resource.ResourceUtil;
045import icy.resource.icon.IcyIcon;
046import icy.system.IcyExceptionHandler;
047import icy.system.thread.ThreadUtil;
048import icy.type.collection.CollectionUtil;
049import icy.util.DateUtil;
050import icy.util.GraphicsUtil;
051import icy.util.StringUtil;
052
053import java.awt.BorderLayout;
054import java.awt.Color;
055import java.awt.Component;
056import java.awt.Dimension;
057import java.awt.Font;
058import java.awt.Graphics;
059import java.awt.Graphics2D;
060import java.awt.Insets;
061import java.awt.event.ActionEvent;
062import java.awt.event.ActionListener;
063import java.awt.event.KeyAdapter;
064import java.awt.event.KeyEvent;
065import java.awt.event.MouseAdapter;
066import java.awt.event.MouseEvent;
067import java.awt.geom.Rectangle2D;
068import java.io.IOException;
069import java.util.ArrayList;
070
071import javax.swing.Box;
072import javax.swing.BoxLayout;
073import javax.swing.JButton;
074import javax.swing.JLabel;
075import javax.swing.JList;
076import javax.swing.JPanel;
077import javax.swing.JScrollPane;
078import javax.swing.JTabbedPane;
079import javax.swing.JTextField;
080import javax.swing.JTextPane;
081import javax.swing.ListSelectionModel;
082import javax.swing.SwingConstants;
083import javax.swing.border.EmptyBorder;
084import javax.swing.border.LineBorder;
085import javax.swing.event.ChangeEvent;
086import javax.swing.event.ChangeListener;
087import javax.swing.text.BadLocationException;
088import javax.swing.text.SimpleAttributeSet;
089import javax.swing.text.StyleConstants;
090import javax.swing.text.StyledDocument;
091
092import org.schwering.irc.lib.IRCModeParser;
093import org.schwering.irc.lib.IRCUser;
094
095public class ChatPanel extends ExternalizablePanel implements InternetAccessListener
096{
097    /**
098     * 
099     */
100    private static final long serialVersionUID = -3449422097073285247L;
101
102    private static final int MAX_SIZE = 1 * 1024 * 1024; // 1 MB
103
104    private static final String DEFAULT_CHANNEL = "#icy";
105
106    private class CustomIRCClient extends IRCClient
107    {
108        public CustomIRCClient(String host, int port, String pass, String nickName, String userName, String realName)
109        {
110            super(host, port, pass, nickName, userName, realName);
111
112            setName("Chat IRC listener");
113        }
114    }
115
116    private class DesktopOverlay extends AbstractDesktopOverlay
117    {
118        final static int FG_ALPHA = 0xC0;
119        final static int BG_ALPHA = 0xA0;
120
121        final Color defaultBgColor;
122        final Color defaultFgColor;
123        Color bgColor;
124        Color fgColor;
125
126        public DesktopOverlay()
127        {
128            super();
129
130            // default text colors
131            defaultFgColor = getFgColor(Color.black);
132            defaultBgColor = getBgColor(Color.lightGray);
133            fgColor = defaultFgColor;
134            bgColor = defaultBgColor;
135        }
136
137        private Color getFgColor(Color c)
138        {
139            return new Color(c.getRed(), c.getGreen(), c.getBlue(), FG_ALPHA);
140        }
141
142        private Color getBgColor(Color c)
143        {
144            return new Color(c.getRed(), c.getGreen(), c.getBlue(), BG_ALPHA);
145        }
146
147        private int setAttribute(Graphics2D g2, CharSequence text, int index)
148        {
149            final int len = text.length();
150
151            // no more text
152            if (index >= len)
153                return len;
154
155            int result = index + 1;
156            int end;
157            Font f;
158
159            switch (text.charAt(index))
160            {
161                case IRCUtil.CHAR_RESET:
162                    // reset to normal
163                    g2.setFont(FontUtil.setStyle(g2.getFont(), Font.PLAIN));
164                    fgColor = defaultFgColor;
165                    bgColor = defaultBgColor;
166                    break;
167
168                case IRCUtil.CHAR_BOLD:
169                    // switch bold
170                    f = g2.getFont();
171                    g2.setFont(FontUtil.setStyle(f, f.getStyle() ^ Font.BOLD));
172                    break;
173
174                case IRCUtil.CHAR_ITALIC:
175                    // switch italic
176                    f = g2.getFont();
177                    g2.setFont(FontUtil.setStyle(f, f.getStyle() ^ Font.ITALIC));
178                    break;
179
180                case IRCUtil.CHAR_COLOR:
181                    end = StringUtil.getNextNonDigitCharIndex(text, result);
182                    // no more than 2 digits to encode color
183                    if ((end == -1) || (end > (result + 2)))
184                        end = Math.min(text.length(), result + 2);
185
186                    // no color info --> restore default
187                    if (end == result)
188                    {
189                        fgColor = defaultFgColor;
190                        bgColor = defaultBgColor;
191                    }
192                    else
193                    {
194                        // get foreground color
195                        fgColor = getFgColor(
196                                IRCUtil.getIRCColor(Integer.parseInt(text.subSequence(result, end).toString())));
197
198                        // update position
199                        result = end;
200
201                        // search if we have background color
202                        if ((result < len) && (text.charAt(result) == ','))
203                        {
204                            result++;
205
206                            end = StringUtil.getNextNonDigitCharIndex(text, result);
207                            // no more than 2 digits to encode color
208                            if ((end == -1) || (end > (result + 2)))
209                                end = Math.min(text.length(), result + 2);
210
211                            // get background color
212                            if (end != result)
213                            {
214                                // we don't want to support background color...
215                                // bgColor =
216                                // getBgColor(IRCUtil.getIRCColor(Integer.parseInt(text.subSequence(result,
217                                // end)
218                                // .toString())));
219
220                                // update position
221                                result = end;
222                            }
223                        }
224                    }
225                    break;
226
227                default:
228                    // System.out.println("code " + Integer.toString(text.charAt(index)));
229                    break;
230            }
231
232            return result;
233        }
234
235        private int firstPreviousIndexOf(char c, int from)
236        {
237            int ind = from;
238            if (ind >= content.length())
239                return -1;
240
241            while (ind >= 0)
242            {
243                if (content.charAt(ind) == c)
244                    return ind;
245
246                ind--;
247            }
248
249            return ind;
250        }
251
252        private int firstNextIndexOf(char c, int from)
253        {
254            final int len = content.length();
255            int ind = from;
256
257            while (ind < len)
258            {
259                if (content.charAt(ind) == c)
260                    return ind;
261
262                ind++;
263            }
264
265            return -1;
266        }
267
268        private boolean isChannelVisible(String channel)
269        {
270            // no channel name --> assume visible
271            if (StringUtil.isEmpty(channel))
272                return true;
273            // private message --> assume visible
274            if (channel.charAt(0) != '#')
275                return true;
276
277            for (String chan : ChatPreferences.getDesktopChannels().split(";"))
278                if (channel.equalsIgnoreCase(fixChannelName(chan)))
279                    return true;
280
281            return false;
282        }
283
284        @Override
285        public void paint(Graphics g, int width, int height)
286        {
287            final Graphics2D g2 = (Graphics2D) g.create();
288
289            // modify to Monospaced font for easy space calculation
290            g2.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
291
292            // fixed size font, every character has the same bounds
293            final Rectangle2D charRect = GraphicsUtil.getStringBounds(g2, "M");
294            final float charHeight = (float) charRect.getHeight();
295            final float charWidth = (float) charRect.getWidth();
296            // keep 20 pixels margins
297            final int charByLine = (int) ((width - 20) / charRect.getWidth());
298
299            // no enough space to draw text
300            if (charByLine <= 0)
301                return;
302
303            // start y position
304            float y = height;
305            // start at last character
306            int position = content.length() - 1;
307
308            while ((position > 0) && (y > 0))
309            {
310                // get paragraph offsets
311                final int start = firstPreviousIndexOf('\n', position - 1) + 1;
312                final int end = position;
313                final int len = end - start;
314
315                // update position
316                position = start - 1;
317
318                // get channel name end position
319                final int chanEnd = firstNextIndexOf(':', start);
320                // display only if this channel is visible on desktop
321                if ((chanEnd != -1) && isChannelVisible(content.substring(start, chanEnd)))
322                {
323
324                    // calculate number of lines taken by the paragraph
325                    int numLineParagraph = len / charByLine;
326                    if ((len % charByLine) != 0)
327                        numLineParagraph++;
328                    // get paragraph height
329                    final float paragraphHeight = numLineParagraph * charHeight;
330
331                    // position to end of paragraph
332                    y -= paragraphHeight;
333
334                    // set default attributes
335                    g2.setFont(FontUtil.setStyle(g2.getFont(), Font.PLAIN));
336                    fgColor = defaultFgColor;
337                    bgColor = defaultBgColor;
338
339                    // process paragraph
340                    int index = start;
341                    while (index < end)
342                    {
343                        final int lineEnd = Math.min(index + charByLine, end);
344                        float x = 10;
345
346                        // process line
347                        while (index < lineEnd)
348                        {
349                            int ctrlIndex = StringUtil.getNextCtrlCharIndex(content, index);
350                            // end of line
351                            if ((ctrlIndex == -1) || (ctrlIndex > lineEnd))
352                                ctrlIndex = lineEnd;
353
354                            // something to draw ?
355                            if (index != ctrlIndex)
356                            {
357                                // get String to draw
358                                final String str = content.substring(index, ctrlIndex);
359
360                                // draw string
361                                g2.setColor(bgColor);
362                                g2.drawString(str, x - 1, y + 1);
363                                g2.setColor(fgColor);
364                                g2.drawString(str, x, y);
365
366                                // set new X position
367                                x += charWidth * str.length();
368                            }
369
370                            if (ctrlIndex < lineEnd)
371                                index = setAttribute(g2, content, ctrlIndex);
372                            else
373                                index = lineEnd;
374                        }
375
376                        // pass to next line
377                        y += charHeight;
378                    }
379
380                    // set position back to end of paragraph
381                    y -= paragraphHeight;
382                }
383            }
384
385            g2.dispose();
386        }
387    }
388
389    private class CustomIRCClientListener extends IRCEventListenerImpl
390    {
391        public CustomIRCClientListener()
392        {
393            super();
394        }
395
396        @Override
397        public void onConnected()
398        {
399            super.onConnected();
400
401            // join default channel
402            client.doJoin(DEFAULT_CHANNEL);
403            // join extras channels
404            for (String extraChannel : ChatPreferences.getExtraChannels().split(";"))
405                if (!StringUtil.isEmpty(extraChannel))
406                    client.doJoin(fixChannelName(extraChannel));
407            // authentication for registered user
408            final String pass = ChatPreferences.getUserPassword();
409            if (!StringUtil.isEmpty(pass))
410                client.doPrivmsg("NickServ", "identify " + ChatPreferences.getNickname() + " " + pass);
411
412            ThreadUtil.invokeLater(new Runnable()
413            {
414                @Override
415                public void run()
416                {
417                    connectButton.setEnabled(true);
418                }
419            });
420
421            refreshGUI();
422        }
423
424        @Override
425        public void onDisconnected()
426        {
427            super.onDisconnected();
428
429            ThreadUtil.invokeLater(new Runnable()
430            {
431                @Override
432                public void run()
433                {
434                    connectButton.setEnabled(true);
435                }
436            });
437
438            refreshGUI();
439            refreshUsers();
440        }
441
442        @Override
443        public void onError(int num, String msg)
444        {
445            super.onError(num, msg);
446
447            switch (num)
448            {
449                case 432:
450                case 433:
451                    // Erroneous Nickname
452                    if (!connectButton.isEnabled())
453                    {
454                        // if we were connecting, we disconnect
455                        disconnect("Incorrect nickname");
456                        if (num == 432)
457                            onReceive(null, null, "Your nickname contains invalid caracters.");
458                    }
459                    refreshGUI();
460                    break;
461
462                case 437:
463                    client.doNick(client.getNick() + "_");
464                    break;
465            }
466        }
467
468        @Override
469        public void onJoin(String chan, IRCUser u)
470        {
471            if (isCurrentUser(u))
472            {
473                // add the channel pane if needed
474                addChannelPane(chan);
475                if (getShowStatusMessages())
476                    onReceive(null, chan, "Welcome to " + IRCUtil.getBoldString(chan.substring(1)) + ".");
477            }
478            else
479            {
480                if (getShowStatusMessages())
481                    onReceive(null, chan, u.getNick() + " joined.");
482            }
483
484            // refresh user list
485            refreshUsers();
486        }
487
488        @Override
489        public void onKick(String chan, IRCUser u, String nickPass, String msg)
490        {
491            super.onKick(chan, u, nickPass, msg);
492
493            // refresh user list
494            refreshUsers();
495        }
496
497        @Override
498        public void onLeave(String chan, IRCUser u, String msg)
499        {
500            if (getShowStatusMessages())
501            {
502                if (StringUtil.isEmpty(msg))
503                    onReceive(null, chan, u.getNick() + " left.");
504                else
505                    onReceive(null, chan, u.getNick() + " left" + " (" + msg + ").");
506            }
507
508            // remove the channel pane if needed
509            if (isCurrentUser(u))
510                removeChannelPane(chan);
511
512            // refresh user list
513            refreshUsers();
514        }
515
516        @Override
517        public void onMode(IRCUser u, String nickPass, String mode)
518        {
519            // ignore mode set message
520            // super.onMode(u, nickPass, mode);
521        }
522
523        @Override
524        public void onMode(String chan, IRCUser u, IRCModeParser mp)
525        {
526            // ignore mode set message
527            // super.onMode(chan, u, mp);
528        }
529
530        @Override
531        public void onNick(IRCUser u, String nickNew)
532        {
533            super.onNick(u, nickNew);
534
535            // update nickname
536            if (isCurrentUser(u))
537                ChatPreferences.setNickname(nickNew);
538            refreshGUI();
539            refreshUsers();
540        }
541
542        @Override
543        public void onNotice(String target, IRCUser u, String msg)
544        {
545            if (msg != null)
546            {
547                if (getShowStatusMessages())
548                {
549                    if (msg.indexOf("Looking up your hostname") != -1)
550                        onReceive(null, null, "Connecting...");
551                }
552
553                // ignore all others notices...
554
555                // else
556                // {
557                // if (msg.indexOf("Checking Ident") != -1)
558                // return;
559                // if (msg.indexOf("your hostname") != -1)
560                // return;
561                // if (msg.indexOf("No Ident response") != -1)
562                // return;
563                //
564                // super.onNotice(target, u, msg);
565                // }
566            }
567        }
568
569        @Override
570        public void onQuit(IRCUser u, String msg)
571        {
572            super.onQuit(u, msg);
573
574            refreshUsers();
575        }
576
577        @Override
578        public void unknown(String a, String b, String c, String d)
579        {
580            onReceive(null, null, d);
581        }
582
583        @Override
584        public void onReply(int num, String value, String msg)
585        {
586            switch (num)
587            {
588                case 353:
589                    // add users to tmp list
590                    tmpUserList.addAll(CollectionUtil.asList(msg.split(" ")));
591                    break;
592
593                case 366:
594                    // end of user list
595                    userList.setListData(tmpUserList.toArray(new String[tmpUserList.size()]));
596                    tmpUserList.clear();
597                    break;
598
599                case 1:
600                case 2:
601                case 3:
602                case 4:
603                case 5:
604                case 250:
605                case 251:
606                case 252:
607                case 253:
608                case 254:
609                case 255:
610                case 256:
611                case 257:
612                case 258:
613                case 259:
614                case 261:
615                case 262:
616                case 263:
617                case 265:
618                case 266:
619                case 372:
620                case 375:
621                case 376:
622                    // ignore
623                    break;
624
625                default:
626                    onReceive(null, null, msg);
627            }
628        }
629
630        @Override
631        public void onReceive(String nick, String target, String msg)
632        {
633            final String n;
634            final String t;
635
636            // ignore close link
637            if (msg.startsWith("Error: Closing Link:"))
638                return;
639
640            // ignore CTCP version request (can come from hacker)
641            if (msg.equals("\001VERSION\001"))
642                return;
643
644            // target is current user --> incoming private message
645            if (isCurrentUser(target))
646            {
647                // get the source name of private message
648                if (StringUtil.isEmpty(nick))
649                    n = "serv";
650                else
651                    n = nick;
652
653                // incoming private message --> use source as target name
654                t = n;
655
656                // add private channel
657                addChannelPane(n);
658            }
659            else
660            {
661                n = nick;
662                t = target;
663            }
664
665            // show message
666            addMessage(n, t, msg);
667        }
668
669        @Override
670        protected boolean getShowStatusMessages()
671        {
672            return ChatPreferences.getShowStatusMessages();
673        }
674    }
675
676    private class ChannelPanel
677    {
678        final String name;
679
680        final JScrollPane scrollPane;
681        final JTextPane editor;
682        final StyledDocument doc;
683
684        public ChannelPanel(String name)
685        {
686            super();
687
688            this.name = name;
689
690            editor = new JTextPane();
691            editor.setEditable(false);
692            doc = editor.getStyledDocument();
693
694            scrollPane = new JScrollPane(editor);
695        }
696    }
697
698    /**
699     * GUI
700     */
701    JPanel panelBottom;
702    CloseableTabbedPane tabPane;
703    JScrollPane usersScrollPane;
704    JList userList;
705    IcyTextField sendEditor;
706    IcyTextField txtNickName;
707    IcyToggleButton connectButton;
708    IcyToggleButton showUserPaneButton;
709    IcyToggleButton desktopOverlayButton;
710    IcyButton advancedButton;
711
712    /**
713     * Desktop GUI
714     */
715    final JPanel desktopPanel;
716    final IcyTextField sendEditorDesktop;
717    final IcyButton hideDesktopChatButton;
718    final DesktopOverlay desktopOverlay;
719
720    /**
721     * IRC client
722     */
723    CustomIRCClient client;
724
725    /**
726     * internals
727     */
728    final ArrayList<ChannelPanel> channelPanes;
729    final ArrayList<String> tmpUserList;
730    final SimpleAttributeSet attributes;
731    final CustomIRCClientListener ircListener;
732    final StringBuilder content;
733    String lastCmd;
734
735    public ChatPanel()
736    {
737        super("Chat room", "chatPanel");
738
739        channelPanes = new ArrayList<ChannelPanel>();
740        tmpUserList = new ArrayList<String>();
741        attributes = new SimpleAttributeSet();
742        content = new StringBuilder();
743        lastCmd = "";
744        client = null;
745
746        // build GUI
747        initialize();
748
749        // add default channel panel (need to be done when base GUI is done)
750        addChannelPane(DEFAULT_CHANNEL);
751
752        // build desktop GUI
753        sendEditorDesktop = new IcyTextField();
754        sendEditorDesktop.addActionListener(new ActionListener()
755        {
756            @Override
757            public void actionPerformed(ActionEvent e)
758            {
759                sendEditContent((IcyTextField) e.getSource());
760            }
761        });
762        sendEditorDesktop.addKeyListener(new KeyAdapter()
763        {
764            @Override
765            public void keyPressed(KeyEvent e)
766            {
767                if (e.getKeyCode() == KeyEvent.VK_UP)
768                    sendEditorDesktop.setText(lastCmd);
769            }
770        });
771
772        hideDesktopChatButton = new IcyButton(new IcyIcon(ResourceUtil.ICON_SQUARE_DOWN, 20));
773        hideDesktopChatButton.setFlat(true);
774        ComponentUtil.setFixedWidth(hideDesktopChatButton, 20);
775        hideDesktopChatButton.setToolTipText("Disable desktop chat overlay");
776        hideDesktopChatButton.addActionListener(new ActionListener()
777        {
778            @Override
779            public void actionPerformed(ActionEvent e)
780            {
781                ChatPreferences.setDesktopOverlay(false);
782                refreshDesktopOverlayState();
783                new ToolTipFrame(
784                        "<b>Desktop chat overlay</b><br><br>" + "You just disabled the desktop chat overlay<br>"
785                                + "but you can always access and enable it<br>" + "from the inspector \"Chat\" tab.",
786                        "chat.overlay");
787            }
788        });
789
790        // desktop bottom panel
791        desktopPanel = GuiUtil.createLineBoxPanel(sendEditorDesktop, Box.createHorizontalStrut(2),
792                Box.createHorizontalGlue(), hideDesktopChatButton);
793
794        desktopOverlay = new DesktopOverlay();
795
796        StyleConstants.setFontFamily(attributes, "arial");
797        StyleConstants.setFontSize(attributes, 11);
798        StyleConstants.setForeground(attributes, Color.black);
799
800        sendEditor.setFont(new Font("arial", 0, 11));
801        sendEditor.addKeyListener(new KeyAdapter()
802        {
803            @Override
804            public void keyPressed(KeyEvent e)
805            {
806                if (e.getKeyCode() == KeyEvent.VK_UP)
807                    sendEditor.setText(lastCmd);
808            }
809        });
810
811        ircListener = new CustomIRCClientListener();
812
813        refreshGUI();
814        refreshDesktopOverlayState();
815
816        addStateListener(new StateListener()
817        {
818            @Override
819            public void stateChanged(ExternalizablePanel source, boolean externalized)
820            {
821                refreshGUI();
822            }
823        });
824
825        // call default internet connection callback to process auto connect
826        if (NetworkUtil.hasInternetAccess())
827            internetUp();
828
829        NetworkUtil.addInternetAccessListener(this);
830    }
831
832    public boolean isConnected()
833    {
834        return (client != null) && client.isConnected();
835    }
836
837    /**
838     * Do IRC connection
839     */
840    public void connect()
841    {
842        if (!isConnected())
843        {
844            ThreadUtil.invokeLater(new Runnable()
845            {
846                @Override
847                public void run()
848                {
849                    // connecting
850                    connectButton.setEnabled(false);
851                    connectButton.setToolTipText("connecting...");
852                }
853            });
854
855            final String nickName = txtNickName.getText();
856            // apply nickname
857            ChatPreferences.setNickname(nickName);
858            String userName = nickName;
859            String realName = ChatPreferences.getRealname();
860
861            if (StringUtil.isEmpty(userName))
862                userName = nickName;
863            if (StringUtil.isEmpty(realName))
864                realName = nickName;
865
866            // remove listener from previous client
867            if (client != null)
868                client.removeListener(ircListener);
869
870            // we need to recreate the client
871            client = new CustomIRCClient(ChatPreferences.getServer(), ChatPreferences.getPort(),
872                    ChatPreferences.getServerPassword(), nickName, userName, realName);
873            client.addListener(ircListener);
874
875            // process connection in a separate thread as it can take sometime
876            new Thread(new Runnable()
877            {
878                @Override
879                public void run()
880                {
881                    try
882                    {
883                        client.connect();
884                    }
885                    catch (IOException e)
886                    {
887                        // error while connecting
888                        IcyExceptionHandler.showErrorMessage(e, false, false);
889                        System.out.println("Cannot connect to chat.");
890                        System.out.println("If you use a proxy, verify you have valid SOCKS settings.");
891
892                        // update GUI
893                        ThreadUtil.invokeLater(new Runnable()
894                        {
895                            @Override
896                            public void run()
897                            {
898                                connectButton.setEnabled(true);
899                                connectButton.setToolTipText("Not connected - Click to connect");
900                            }
901                        });
902                    }
903                }
904            }, "IRC client connection").start();
905        }
906    }
907
908    public void disconnect(String message)
909    {
910        if (isConnected())
911        {
912            ThreadUtil.invokeLater(new Runnable()
913            {
914                @Override
915                public void run()
916                {
917                    // closing connection
918                    connectButton.setEnabled(false);
919                    connectButton.setToolTipText("closing connexion...");
920                }
921            });
922
923            client.doQuit(message);
924        }
925    }
926
927    protected void sendEditContent(JTextField txtField)
928    {
929        final String text = txtField.getText();
930
931        if (isConnected() && !StringUtil.isEmpty(text))
932        {
933            // send from desktop editor ?
934            if (txtField == sendEditorDesktop)
935                // send text to main default channel
936                client.send(DEFAULT_CHANNEL, text);
937            else
938                // send text to current target
939                client.send(getCurrentChannel(), text);
940        }
941
942        txtField.setText("");
943        lastCmd = text;
944    }
945
946    /**
947     * Build the panel
948     */
949    private void initialize()
950    {
951        setLayout(new BorderLayout(0, 0));
952
953        tabPane = new CloseableTabbedPane(SwingConstants.TOP, JTabbedPane.SCROLL_TAB_LAYOUT);
954        tabPane.addCloseableTabbedPaneListener(new CloseableTabbedPaneListener()
955        {
956            @Override
957            public void tabClosed(int index, String title)
958            {
959                // it was a channel tab, leave channel
960                if (isConnected() && title.startsWith("#"))
961                    client.doPart(title);
962                else
963                    // directly remove the channel pane
964                    removeChannelPane(title);
965            }
966
967            @Override
968            public boolean tabClosing(int index, String title)
969            {
970                return true;
971            }
972        });
973        tabPane.addChangeListener(new ChangeListener()
974        {
975            @Override
976            public void stateChanged(ChangeEvent e)
977            {
978                // set back default tab color
979                tabPane.setBackgroundAt(tabPane.getSelectedIndex(), tabPane.getBackground());
980                refreshUsers();
981            }
982        });
983        add(tabPane, BorderLayout.CENTER);
984
985        usersScrollPane = new JScrollPane();
986        usersScrollPane.setPreferredSize(new Dimension(130, 200));
987
988        JLabel lblUtilisateur = new JLabel("Users");
989        lblUtilisateur.setFont(new Font("Tahoma", Font.BOLD, 11));
990        lblUtilisateur.setBorder(new LineBorder(Color.GRAY, 1, true));
991        lblUtilisateur.setHorizontalAlignment(SwingConstants.CENTER);
992        usersScrollPane.setColumnHeaderView(lblUtilisateur);
993
994        userList = new JList();
995        userList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
996        userList.addMouseListener(new MouseAdapter()
997        {
998            @Override
999            public void mouseClicked(MouseEvent e)
1000            {
1001                if (!isConnected())
1002                    return;
1003
1004                // double click
1005                if (e.getClickCount() == 2)
1006                {
1007                    final int index = userList.locationToIndex(e.getPoint());
1008
1009                    if (index != -1)
1010                    {
1011                        final String nick = (String) userList.getSelectedValue();
1012
1013                        // add private chat nick pane
1014                        if (!isCurrentUser(nick) && !StringUtil.isEmpty(nick))
1015                            addChannelPane(nick);
1016                    }
1017                }
1018            }
1019        });
1020        userList.setToolTipText("Double click on an username to send private message");
1021
1022        usersScrollPane.setViewportView(userList);
1023
1024        panelBottom = new JPanel();
1025        add(panelBottom, BorderLayout.SOUTH);
1026        panelBottom.setLayout(new BorderLayout(0, 0));
1027
1028        sendEditor = new IcyTextField();
1029        sendEditor.addActionListener(new ActionListener()
1030        {
1031            @Override
1032            public void actionPerformed(ActionEvent e)
1033            {
1034                sendEditContent((IcyTextField) e.getSource());
1035            }
1036        });
1037        sendEditor.setColumns(10);
1038        panelBottom.add(sendEditor, BorderLayout.SOUTH);
1039
1040        JPanel topPanel = new JPanel();
1041        topPanel.setBorder(new EmptyBorder(0, 0, 2, 0));
1042        add(topPanel, BorderLayout.NORTH);
1043
1044        topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.LINE_AXIS));
1045
1046        txtNickName = new IcyTextField();
1047        txtNickName.setMaximumSize(new Dimension(2147483647, 24));
1048        txtNickName.setText(ChatPreferences.getNickname());
1049        txtNickName.setToolTipText("Nick name");
1050        txtNickName.setColumns(10);
1051        topPanel.add(txtNickName);
1052
1053        Component horizontalStrut_3 = Box.createHorizontalStrut(4);
1054        topPanel.add(horizontalStrut_3);
1055
1056        JButton btnSetNickName = new JButton("Set");
1057        btnSetNickName.setFocusPainted(false);
1058        btnSetNickName.setPreferredSize(new Dimension(40, 23));
1059        btnSetNickName.setMaximumSize(new Dimension(40, 23));
1060        btnSetNickName.setMinimumSize(new Dimension(40, 23));
1061        btnSetNickName.setMargin(new Insets(2, 8, 2, 8));
1062        btnSetNickName.addActionListener(new ActionListener()
1063        {
1064            @Override
1065            public void actionPerformed(ActionEvent e)
1066            {
1067                final String nickName = txtNickName.getText();
1068
1069                if (isConnected())
1070                    client.doNick(nickName);
1071                else
1072                    ChatPreferences.setNickname(nickName);
1073            }
1074        });
1075        btnSetNickName.setToolTipText("Set nick name");
1076        topPanel.add(btnSetNickName);
1077
1078        Component horizontalGlue = Box.createHorizontalGlue();
1079        topPanel.add(horizontalGlue);
1080
1081        Component horizontalStrut_2 = Box.createHorizontalStrut(10);
1082        topPanel.add(horizontalStrut_2);
1083
1084        connectButton = new IcyToggleButton(new IcyIcon(ResourceUtil.ICON_ON_OFF, 20));
1085        connectButton.setFocusPainted(false);
1086        connectButton.setMaximumSize(new Dimension(32, 32));
1087        connectButton.setMinimumSize(new Dimension(24, 24));
1088        connectButton.setPreferredSize(new Dimension(24, 24));
1089        connectButton.setToolTipText("Connect");
1090        connectButton.addActionListener(new ActionListener()
1091        {
1092            @Override
1093            public void actionPerformed(ActionEvent e)
1094            {
1095                if (connectButton.isSelected())
1096                {
1097                    if (!NetworkUtil.hasInternetAccess())
1098                    {
1099                        new AnnounceFrame("You need internet connection to connect to the chat.", 10);
1100                        connectButton.setSelected(false);
1101                    }
1102                    else
1103                        connect();
1104                }
1105                else
1106                    disconnect("Manual disconnect");
1107            }
1108        });
1109        topPanel.add(connectButton);
1110
1111        Component horizontalStrut_1 = Box.createHorizontalStrut(2);
1112        topPanel.add(horizontalStrut_1);
1113
1114        showUserPaneButton = new IcyToggleButton(new IcyIcon("user", 20));
1115        showUserPaneButton.setFocusPainted(false);
1116        showUserPaneButton.setMaximumSize(new Dimension(32, 32));
1117        showUserPaneButton.setSelected(ChatPreferences.getShowUsersPanel());
1118        showUserPaneButton.setMinimumSize(new Dimension(24, 24));
1119        showUserPaneButton.setPreferredSize(new Dimension(24, 24));
1120        showUserPaneButton.setToolTipText("Show connected users");
1121        showUserPaneButton.addActionListener(new ActionListener()
1122        {
1123            @Override
1124            public void actionPerformed(ActionEvent e)
1125            {
1126                final boolean visible = showUserPaneButton.isSelected();
1127
1128                ChatPreferences.setShowUsersPanel(visible);
1129                refreshGUI();
1130            }
1131        });
1132        topPanel.add(showUserPaneButton);
1133
1134        Component horizontalStrut_5 = Box.createHorizontalStrut(2);
1135        topPanel.add(horizontalStrut_5);
1136
1137        desktopOverlayButton = new IcyToggleButton(new IcyIcon(ResourceUtil.ICON_CHAT, 20));
1138        desktopOverlayButton.setFocusPainted(false);
1139        desktopOverlayButton.setMaximumSize(new Dimension(32, 32));
1140        desktopOverlayButton.setSelected(ChatPreferences.getDesktopOverlay());
1141        desktopOverlayButton.setMinimumSize(new Dimension(24, 24));
1142        desktopOverlayButton.setPreferredSize(new Dimension(24, 24));
1143        desktopOverlayButton.setToolTipText("Enabled chat on desktop");
1144        desktopOverlayButton.addActionListener(new ActionListener()
1145        {
1146            @Override
1147            public void actionPerformed(ActionEvent e)
1148            {
1149                final boolean visible = desktopOverlayButton.isSelected();
1150
1151                ChatPreferences.setDesktopOverlay(visible);
1152                refreshDesktopOverlayState();
1153            }
1154        });
1155        topPanel.add(desktopOverlayButton);
1156
1157        Component horizontalStrut_6 = Box.createHorizontalStrut(2);
1158        topPanel.add(horizontalStrut_6);
1159
1160        advancedButton = new IcyButton(new IcyIcon(ResourceUtil.ICON_COG, 20));
1161        advancedButton.setFocusPainted(false);
1162        advancedButton.setMaximumSize(new Dimension(32, 32));
1163        advancedButton.setMinimumSize(new Dimension(24, 24));
1164        advancedButton.setPreferredSize(new Dimension(24, 24));
1165        advancedButton.setToolTipText("Advanced settings");
1166        advancedButton.addActionListener(new ActionListener()
1167        {
1168            @Override
1169            public void actionPerformed(ActionEvent e)
1170            {
1171                new PreferenceFrame(ChatPreferencePanel.NODE_NAME);
1172            }
1173        });
1174        topPanel.add(advancedButton);
1175    }
1176
1177    protected String fixChannelName(String channel)
1178    {
1179        if (!StringUtil.isEmpty(channel) && (channel.charAt(0) != '#'))
1180            return "#" + channel;
1181
1182        return channel;
1183    }
1184
1185    public void addChannelPane(final String channel)
1186    {
1187        // already exists...
1188        if (getChannelPaneIndex(channel) != -1)
1189            return;
1190
1191        ThreadUtil.invokeLater(new Runnable()
1192        {
1193            @Override
1194            public void run()
1195            {
1196                final ChannelPanel channelPane = new ChannelPanel(channel);
1197
1198                // add to list
1199                channelPanes.add(channelPane);
1200
1201                // and add to gui
1202                int index = tabPane.indexOfTab(channel);
1203                // add only if not already present (should alway be the case)
1204                if (index == -1)
1205                {
1206                    tabPane.addTab(channel, channelPane.scrollPane);
1207                    // get index
1208                    index = tabPane.indexOfTab(channel);
1209                    // default channel cannot be closed
1210                    if (channel.equals(DEFAULT_CHANNEL))
1211                        tabPane.setTabClosable(index, false);
1212                    tabPane.setSelectedIndex(index);
1213                }
1214            }
1215        });
1216    }
1217
1218    public void removeChannelPane(final String channel)
1219    {
1220        final int ind = getChannelPaneIndex(channel);
1221
1222        // channel exists --> remove it
1223        if (ind != -1)
1224        {
1225            ThreadUtil.invokeLater(new Runnable()
1226            {
1227                @Override
1228                public void run()
1229                {
1230                    channelPanes.remove(ind);
1231
1232                    // remove from tabbed pane if needed
1233                    final int indTab = tabPane.indexOfTab(channel);
1234                    if (indTab != -1)
1235                        tabPane.removeTabAt(indTab);
1236                }
1237            });
1238        }
1239    }
1240
1241    public int getChannelPaneIndex(String channel)
1242    {
1243        final String c;
1244
1245        // empty channel means default channel
1246        if (StringUtil.isEmpty(channel))
1247            c = DEFAULT_CHANNEL;
1248        else
1249            c = channel;
1250
1251        for (int i = 0; i < channelPanes.size(); i++)
1252        {
1253            final ChannelPanel cp = channelPanes.get(i);
1254
1255            if (cp.name.equalsIgnoreCase(c))
1256                return i;
1257        }
1258
1259        return -1;
1260    }
1261
1262    public ChannelPanel getChannelPane(String channel)
1263    {
1264        final int ind = getChannelPaneIndex(channel);
1265
1266        if (ind != -1)
1267            return channelPanes.get(ind);
1268
1269        return null;
1270    }
1271
1272    /**
1273     * Returns the current visible channel (tab channel visible).
1274     */
1275    public String getCurrentChannel()
1276    {
1277        final int ind = tabPane.getSelectedIndex();
1278
1279        if (ind != -1)
1280            return tabPane.getTitleAt(ind);
1281
1282        return null;
1283    }
1284
1285    /**
1286     * Mark channel pane with blue color.
1287     */
1288    protected void markChannelPane(String channel)
1289    {
1290        final int index = getChannelPaneIndex(channel);
1291
1292        if ((index != -1) && (tabPane.getSelectedIndex() != index))
1293        {
1294            ThreadUtil.invokeLater(new Runnable()
1295            {
1296                @Override
1297                public void run()
1298                {
1299                    // change output console tab color when new data
1300                    if (index < tabPane.getTabCount())
1301                        tabPane.setBackgroundAt(index, Color.blue);
1302                }
1303            });
1304        }
1305    }
1306
1307    public JTextPane getChannelEditor(String channel)
1308    {
1309        final ChannelPanel cp = getChannelPane(channel);
1310
1311        if (cp != null)
1312            return cp.editor;
1313
1314        return null;
1315    }
1316
1317    public StyledDocument getChannelDocument(String channel)
1318    {
1319        final ChannelPanel cp = getChannelPane(channel);
1320
1321        if (cp != null)
1322            return cp.doc;
1323
1324        return null;
1325    }
1326
1327    protected boolean isCurrentUser(String nick)
1328    {
1329        return StringUtil.equals(nick, ChatPreferences.getNickname());
1330    }
1331
1332    protected boolean isCurrentUser(IRCUser u)
1333    {
1334        if (u != null)
1335            return isCurrentUser(u.getNick());
1336
1337        return false;
1338    }
1339
1340    // int getUsersScrollPaneWidth()
1341    // {
1342    // int result = usersScrollPane.getSize().width;
1343    // if (result == 0)
1344    // return usersScrollPane.getPreferredSize().width;
1345    // return result;
1346    // }
1347    //
1348    // int getCurrentWidth()
1349    // {
1350    // int result = getSize().width;
1351    // if (result == 0)
1352    // return getPreferredSize().width;
1353    // return result;
1354    // }
1355
1356    void addMessage(final String nick, final String target, final String msg)
1357    {
1358        ThreadUtil.invokeLater(new Runnable()
1359        {
1360            @Override
1361            public void run()
1362            {
1363                final String channel;
1364                final String nickStr;
1365
1366                if (StringUtil.isEmpty(target))
1367                    channel = DEFAULT_CHANNEL;
1368                else
1369                    channel = target;
1370
1371                if (StringUtil.isEmpty(nick))
1372                    nickStr = "";
1373                else
1374                    nickStr = "<" + nick + "> ";
1375
1376                final String timeStr = DateUtil.now("[HH:mm] ");
1377
1378                synchronized (content)
1379                {
1380                    content.append(channel + ": " + timeStr + nickStr + msg + "\n");
1381
1382                    // limit to maximum size
1383                    if (content.length() > MAX_SIZE)
1384                        content.delete(0, content.length() - MAX_SIZE);
1385
1386                    refreshDesktopOverlay();
1387                }
1388
1389                try
1390                {
1391                    final JTextPane editor = getChannelEditor(channel);
1392                    final StyledDocument doc = getChannelDocument(channel);
1393
1394                    if ((editor != null) && (doc != null))
1395                    {
1396                        synchronized (editor)
1397                        {
1398                            IRCUtil.insertString(timeStr + nickStr + msg + "\n", doc, attributes);
1399
1400                            // limit to maximum size
1401                            if (doc.getLength() > MAX_SIZE)
1402                                doc.remove(0, doc.getLength() - MAX_SIZE);
1403
1404                            editor.setCaretPosition(doc.getLength());
1405                        }
1406                    }
1407                }
1408                catch (BadLocationException e)
1409                {
1410                    e.printStackTrace();
1411                }
1412
1413                // mark channel pane color
1414                markChannelPane(channel);
1415            }
1416        });
1417    }
1418
1419    /**
1420     * Refresh GUI state
1421     */
1422    void refreshGUI()
1423    {
1424        ThreadUtil.invokeLater(new Runnable()
1425        {
1426            @Override
1427            public void run()
1428            {
1429                if (isConnected())
1430                {
1431                    final String nick = client.getNick();
1432
1433                    if (!StringUtil.equals(txtNickName.getText(), nick))
1434                        txtNickName.setText(nick);
1435
1436                    sendEditor.setEditable(true);
1437                    sendEditorDesktop.setEditable(true);
1438                    connectButton.setSelected(true);
1439                    connectButton.setToolTipText("Connected - Click to disconnect");
1440                }
1441                else
1442                {
1443                    sendEditor.setEditable(false);
1444                    sendEditorDesktop.setEditable(false);
1445                    connectButton.setSelected(false);
1446                    connectButton.setToolTipText("Not connected - Click to connect");
1447                }
1448
1449                // user panel visible
1450                remove(usersScrollPane);
1451                panelBottom.remove(usersScrollPane);
1452                if (ChatPreferences.getShowUsersPanel())
1453                {
1454                    if ((getWidth() * 1.5) > getHeight())
1455                        add(usersScrollPane, BorderLayout.EAST);
1456                    else
1457                        panelBottom.add(usersScrollPane, BorderLayout.CENTER);
1458                }
1459
1460                validate();
1461            }
1462        });
1463    }
1464
1465    /**
1466     * Refresh users list
1467     */
1468    void refreshUsers()
1469    {
1470        if (isConnected())
1471        {
1472            final String channel = getCurrentChannel();
1473
1474            if (!StringUtil.isEmpty(channel))
1475            {
1476                // private channel ?
1477                if (channel.charAt(0) != '#')
1478                    // display current user and channel name as users
1479                    userList.setListData(new String[] {ChatPreferences.getNickname(), channel});
1480                else
1481                    client.doNames(channel);
1482            }
1483        }
1484        else
1485            userList.setListData(new String[0]);
1486    }
1487
1488    /**
1489     * Refresh desktop overlay state
1490     */
1491    public void refreshDesktopOverlayState()
1492    {
1493        final MainFrame mainFrame = Icy.getMainInterface().getMainFrame();
1494        final IcyDesktopPane desktopPane = Icy.getMainInterface().getDesktopPane();
1495
1496        if ((mainFrame != null) && (desktopPane != null))
1497        {
1498            final JPanel centerPanel = mainFrame.getCenterPanel();
1499
1500            // desktop overlay enable ?
1501            if (ChatPreferences.getDesktopOverlay())
1502            {
1503                // add desktop overlay
1504                desktopPane.addOverlay(desktopOverlay);
1505                centerPanel.add(desktopPanel, BorderLayout.SOUTH);
1506                centerPanel.revalidate();
1507            }
1508            else
1509            {
1510                // remove desktop overlay
1511                desktopPane.removeOverlay(desktopOverlay);
1512                centerPanel.remove(desktopPanel);
1513                centerPanel.revalidate();
1514            }
1515
1516            // refresh desktop
1517            desktopPane.repaint();
1518        }
1519
1520        desktopOverlayButton.setSelected(ChatPreferences.getDesktopOverlay());
1521    }
1522
1523    /**
1524     * Refresh desktop overlay
1525     */
1526    void refreshDesktopOverlay()
1527    {
1528        final IcyDesktopPane desktopPane = Icy.getMainInterface().getDesktopPane();
1529
1530        // refresh desktop overlay
1531        if ((desktopPane != null) && ChatPreferences.getDesktopOverlay())
1532            desktopPane.repaint();
1533    }
1534
1535    @Override
1536    public void internetUp()
1537    {
1538        if (!isConnected() && ChatPreferences.getAutoConnect())
1539            connect();
1540    }
1541
1542    @Override
1543    public void internetDown()
1544    {
1545        // disconnect("Connection lost");
1546    }
1547}