Don't let a selection affect the scrollbar position
[konversation:konversation.git] / src / ircview.cpp
1 // -*- mode: c++; c-file-style: "bsd"; c-basic-offset: 4; tabs-width: 4; indent-tabs-mode: nil -*-
2
3 /*
4   This program is free software; you can redistribute it and/or modify
5   it under the terms of the GNU General Public License as published by
6   the Free Software Foundation; either version 2 of the License, or
7   (at your option) any later version.
8 */
9
10 /*
11   Copyright (C) 2002 Dario Abatianni <eisfuchs@tigress.com>
12   Copyright (C) 2005-2007 Peter Simonsson <psn@linux.se>
13   Copyright (C) 2006 Eike Hein <hein@kde.org>
14 */
15
16 #include "ircview.h"
17 #include "channel.h"
18 #include "dccchat.h"
19 #include "konversationapplication.h"
20 #include "konversationmainwindow.h"
21 #include "viewcontainer.h"
22 #include "highlight.h"
23 #include "server.h"
24 #include "konversationsound.h"
25 #include "common.h"
26 #include "emoticon.h"
27 #include "notificationhandler.h"
28
29 #include <private/qrichtext_p.h>
30 #include <qstylesheet.h>
31 #include <qstringlist.h>
32 #include <qregexp.h>
33 #include <qtextbrowser.h>
34 #include <qclipboard.h>
35 #include <qbrush.h>
36 #include <qevent.h>
37 #include <qdragobject.h>
38 #include <qpopupmenu.h>
39 #include <qwhatsthis.h>
40 #include <qmap.h>
41 #include <qcolor.h>
42 #include <qscrollbar.h>
43 #include <qcursor.h>
44
45 #include <dcopref.h>
46 #include <dcopclient.h>
47 #include <kmessagebox.h>
48 #include <klocale.h>
49 #include <kurl.h>
50 #include <kurldrag.h>
51 #include <kbookmark.h>
52 #include <kbookmarkmanager.h>
53 #include <kdeversion.h>
54 #include <kstandarddirs.h>
55 #include <krun.h>
56 #include <kprocess.h>
57 #include <kiconloader.h>
58 #include <kshell.h>
59 #include <kpopupmenu.h>
60 #include <kaction.h>
61 #include <kglobalsettings.h>
62 #include <kdebug.h>
63 #include <kmenubar.h>
64 #include <kfiledialog.h>
65 #include <kio/job.h>
66 #include <kstdaccel.h>
67 #include <kglobal.h>
68
69
70 IRCView::IRCView(QWidget* parent, Server* newServer) : KTextBrowser(parent)
71 {
72     m_copyUrlMenu = false;
73     m_resetScrollbar = true;
74     m_offset = 0;
75     m_mousePressed = false;
76     m_isOnNick = false;
77     m_isOnChannel = false;
78     m_chatWin = 0;
79     m_findParagraph=0;
80     m_findIndex=0;
81     m_lastInsertionWasLine = false;
82
83     setAutoFormatting(QTextEdit::AutoNone);
84     setUndoRedoEnabled(0);
85     setLinkUnderline(false);
86     setVScrollBarMode(AlwaysOn);
87     setHScrollBarMode(AlwaysOff);
88     setWrapPolicy(QTextEdit::AtWordOrDocumentBoundary);
89     setNotifyClick(true);
90     setFocusPolicy(QWidget::ClickFocus);
91
92     // set basic style sheet for <p> to make paragraph spacing possible
93     QStyleSheet* sheet=new QStyleSheet(this,"ircview_style_sheet");
94     new QStyleSheetItem(sheet,"p");
95     setStyleSheet(sheet);
96
97     m_popup = new QPopupMenu(this,"ircview_context_menu");
98     toggleMenuBarSeparator = m_popup->insertSeparator();
99     m_popup->setItemVisible(toggleMenuBarSeparator, false);
100     m_popup->insertItem(SmallIconSet("editcopy"),i18n("&Copy"),Copy);
101     m_popup->insertItem(i18n("Select All"),SelectAll);
102     m_popup->insertSeparator();
103     m_popup->insertItem(SmallIcon("find"),i18n("Find Text..."),Search);
104
105     setServer(newServer);
106
107     setViewBackground(Preferences::color(Preferences::TextViewBackground),QString());
108
109     if (Preferences::customTextFont())
110         setFont(Preferences::textFont());
111     else
112         setFont(KGlobalSettings::generalFont());
113
114     connect(this, SIGNAL(highlighted(const QString&)), this, SLOT(highlightedSlot(const QString&)));
115 }
116
117 IRCView::~IRCView()
118 {
119     delete m_popup;
120 }
121
122 void IRCView::updateStyleSheet()
123 {
124     // set style sheet for <p> to define paragraph spacing
125     QStyleSheet* sheet = styleSheet();
126
127     if(!sheet)
128         return;
129
130     int paragraphSpacing;
131
132     if(Preferences::useParagraphSpacing())
133         paragraphSpacing=Preferences::paragraphSpacing();
134     else
135         paragraphSpacing = 0;
136
137     QStyleSheetItem* style=sheet->item("p");
138
139     if(!sheet)
140     {
141         kdDebug() << "IRCView::updateStyleSheet(): style == 0!" << endl;
142         return;
143     }
144
145     style->setDisplayMode(QStyleSheetItem::DisplayBlock);
146     style->setMargin(QStyleSheetItem::MarginVertical,paragraphSpacing);
147     style->setSelfNesting(false);
148 }
149
150 void IRCView::setViewBackground(const QColor& backgroundColor, const QString& pixmapName)
151 {
152     QPixmap backgroundPixmap;
153     backgroundPixmap.load(pixmapName);
154
155     if(backgroundPixmap.isNull())
156     {
157         setPaper(backgroundColor);
158     }
159     else
160     {
161         QBrush backgroundBrush;
162         backgroundBrush.setColor(backgroundColor);
163         backgroundBrush.setPixmap(backgroundPixmap);
164         setPaper(backgroundBrush);
165     }
166 }
167
168 void IRCView::setServer(Server* newServer)
169 {
170     m_server = newServer;
171
172     if (newServer)
173     {
174         KAction *action = newServer->getViewContainer()->actionCollection()->action("open_logfile");
175         Q_ASSERT(action);
176         if(!action) return;
177         m_popup->insertSeparator();
178         action->plug(m_popup);
179     }
180
181 }
182
183 const QString& IRCView::getContextNick() const
184 {
185     return m_currentNick;
186 }
187
188 void IRCView::clearContextNick()
189 {
190     m_currentNick = QString();
191 }
192
193 void IRCView::clear()
194 {
195     m_buffer = QString();
196     KTextBrowser::setText("");
197 }
198
199 void IRCView::highlightedSlot(const QString& _link)
200 {
201     QString link = _link;
202     // HACK Replace % with \x03 in the url to keep Qt from doing stupid things
203     link = link.replace ('\x03', "%");
204     //Hack to handle the fact that we get a decoded url
205     link = KURL::fromPathOrURL(link).url();
206
207     // HACK:Use space as a placeholder for \ as Qt tries to be clever and does a replace to / in urls in QTextEdit
208     if(link.startsWith("#"))
209     {
210         link = link.replace(' ', "\\");
211     }
212
213     //we just saw this a second ago.  no need to reemit.
214     if (link == m_lastStatusText && !link.isEmpty())
215         return;
216
217     // remember current URL to overcome link clicking problems in QTextBrowser
218     m_highlightedURL = link;
219
220     if (link.isEmpty())
221     {
222         if (!m_lastStatusText.isEmpty()) 
223         {
224             emit clearStatusBarTempText();
225             m_lastStatusText = QString();
226         }
227     } else
228     {
229         m_lastStatusText = link;
230     }
231
232     if(!link.startsWith("#"))
233     {
234         m_isOnNick = false;
235         m_isOnChannel = false;
236
237         if (!link.isEmpty()) {
238             //link therefore != m_lastStatusText  so emit with this new text
239             emit setStatusBarTempText(link);
240         }
241         if (link.isEmpty() && m_copyUrlMenu)
242         {
243             m_popup->removeItem(CopyUrl);
244             m_popup->removeItem(Bookmark);
245             m_popup->removeItem(SaveAs);
246             m_copyUrlMenu = false;
247         }
248         else if (!link.isEmpty() && !m_copyUrlMenu)
249         {
250             m_popup->insertItem(i18n("Copy URL to Clipboard"),CopyUrl,1);
251             m_popup->insertItem(i18n("Add to Bookmarks"),Bookmark,2);
252             m_popup->insertItem(i18n("Save Link As..."), SaveAs, 3);
253             m_copyUrlMenu = true;
254             m_urlToCopy = link;
255         }
256     }
257     else if (link.startsWith("#") && !link.startsWith("##"))
258     {
259         m_currentNick = link.mid(1);
260         m_nickPopup->changeTitle(m_nickPopupId,m_currentNick);
261         m_isOnNick = true;
262         emit setStatusBarTempText(i18n("Open a query with %1").arg(m_currentNick));
263     }
264     else
265     {
266         // link.startsWith("##")
267         m_currentChannel = link.mid(1);
268
269         QString prettyId = m_currentChannel;
270
271         if (prettyId.length()>15)
272         {
273             prettyId.truncate(15);
274             prettyId.append("...");
275         }
276
277         m_channelPopup->changeTitle(m_channelPopupId,prettyId);
278         m_isOnChannel = true;
279         emit setStatusBarTempText(i18n("Join the channel %1").arg(m_currentChannel));
280     }
281 }
282
283 void IRCView::openLink(const QString& url, bool newTab)
284 {
285     if (!url.isEmpty() && !url.startsWith("#"))
286     {
287         // Always use KDE default mailer.
288         if (!Preferences::useCustomBrowser() || url.startsWith("mailto:"))
289         {
290             if(newTab && !url.startsWith("mailto:"))
291             {
292                 QCString foundApp, foundObj;
293                 QByteArray data;
294                 QDataStream str(data, IO_WriteOnly);
295                 if( KApplication::dcopClient()->findObject("konqueror*", "konqueror-mainwindow*",
296                     "windowCanBeUsedForTab()", data, foundApp, foundObj, false, 3000))
297                 {
298                     DCOPRef ref(foundApp, foundObj);
299                     ref.call("newTab", url);
300                 } else
301                     new KRun(KURL::fromPathOrURL(url));
302             } else
303                 new KRun(KURL::fromPathOrURL(url));
304         }
305         else
306         {
307             QString cmd = Preferences::webBrowserCmd();
308             cmd.replace("%u", url);
309             KProcess *proc = new KProcess;
310             QStringList cmdAndArgs = KShell::splitArgs(cmd);
311             *proc << cmdAndArgs;
312             //      This code will also work, but starts an extra shell process.
313             //      kdDebug() << "IRCView::urlClickSlot(): cmd = " << cmd << endl;
314             //      *proc << cmd;
315             //      proc->setUseShell(true);
316             proc->start(KProcess::DontCare);
317             delete proc;
318         }
319     }
320     //FIXME: Don't do channel links in DCC Chats to begin with since they don't have a server.
321     else if (url.startsWith("##") && m_server && m_server->isConnected())
322     {
323         QString channel(url);
324         channel.replace("##", "#");
325         m_server->sendJoinCommand(channel);
326     }
327     //FIXME: Don't do user links in DCC Chats to begin with since they don't have a server.
328     else if (url.startsWith("#") && m_server && m_server->isConnected()) 
329     {
330         QString recipient(url);
331         recipient.remove("#");
332         NickInfoPtr nickInfo = m_server->obtainNickInfo(recipient);
333         m_server->addQuery(nickInfo, true /*we initiated*/);
334     }
335 }
336
337 void IRCView::replaceDecoration(QString& line, char decoration, char replacement)
338 {
339     int pos;
340     bool decorated = false;
341
342     while((pos=line.find(decoration))!=-1)
343     {
344         line.replace(pos,1,(decorated) ? QString("</%1>").arg(replacement) : QString("<%1>").arg(replacement));
345         decorated = !decorated;
346     }
347 }
348
349 QString IRCView::filter(const QString& line, const QString& defaultColor, const QString& whoSent,
350 bool doHighlight, bool parseURL, bool self)
351 {
352     QString filteredLine(line);
353     KonversationApplication* konvApp = static_cast<KonversationApplication*>(kapp);
354
355     //Since we can't turn off whitespace simplification withouteliminating text wrapping,
356     //  if the line starts with a space turn it into a non-breaking space.
357     //    (which magically turns back into a space on copy)
358
359     if (filteredLine[0]==' ')
360         filteredLine[0]='\xA0';
361
362     // TODO: Use QStyleSheet::escape() here
363     // Replace all < with &lt;
364     filteredLine.replace("<","\x0blt;");
365     // Replace all > with &gt;
366     filteredLine.replace(">", "\x0bgt;");
367
368     #if 0
369     if(!Preferences::disableExpansion())
370     {
371         QRegExp boldRe("\\*\\*([a-zA-Z0-9]+)\\*\\*");
372         QRegExp underRe("\\_\\_([a-zA-Z0-9]+)\\_\\_");
373         int position = 0;
374         QString replacement;
375
376         while( position >= 0)
377         {
378             position = boldRe.search(filteredLine, position);
379             if( position > -1)
380             {
381                 replacement = boldRe.cap(1);
382                 replacement = "\x02"+replacement+"\x02";
383                 filteredLine.replace(position,replacement.length()+2,replacement);
384             }
385             position += boldRe.matchedLength();
386         }
387
388         position = 0;
389         while( position >= 0)
390         {
391             position = underRe.search(filteredLine, position);
392             if( position > -1)
393             {
394                 replacement = underRe.cap(1);
395                 replacement = "\x1f"+replacement+"\x1f";
396                 filteredLine.replace(position,replacement.length()+2,replacement);
397             }
398             position += underRe.matchedLength();
399         }
400     }
401     #endif
402
403     // Replace all 0x03 without color number (reset color) with \0x031,0 or \0x030,1, depending on which one fits
404     // with the users chosen colours, based on the relative brightness. TODO defaultColor needs explanation
405
406     bool inverted = false;                        // TODO this flag should be stored somewhere
407     {
408         QColor fg(Preferences::color(Preferences::ChannelMessage));
409         QColor  bg(Preferences::color(Preferences::TextViewBackground));
410
411         int h = 0, s = 0,fv = 0,bv = 0;
412         fg.getHsv(&h,&s,&fv);
413         bg.getHsv(&h,&s,&bv);
414
415         if (bv <= fv)
416         {
417             inverted = false;
418         }
419     }
420
421     if(inverted)
422     {
423         filteredLine.replace(QRegExp("\003([^0-9]|$)"),"\0030,1\\1");
424     }
425     else
426     {
427         filteredLine.replace(QRegExp("\003([^0-9]|$)"),"\0031,0\\1");
428     }
429
430     if(filteredLine.find("\x07") != -1)
431     {
432         if(Preferences::beep())
433         {
434             kapp->beep();
435         }
436     }
437
438     // replace \003 and \017 codes with rich text color codes
439     // captures          1    2                   23 4                   4 3     1
440     QRegExp colorRegExp("(\003([0-9]|0[0-9]|1[0-5])(,([0-9]|0[0-9]|1[0-5])|)|\017)");
441
442     int pos;
443     bool allowColors = Preferences::allowColorCodes();
444     bool firstColor = true;
445     QString colorString;
446
447     while((pos=colorRegExp.search(filteredLine))!=-1)
448     {
449         if(!allowColors)
450         {
451             colorString = QString();
452         }
453         else
454         {
455             colorString = (firstColor) ? QString::null : QString("</font>");
456
457             // reset colors on \017 to default value
458             if(colorRegExp.cap(1) == "\017")
459                 colorString += "<font color=\""+defaultColor+"\">";
460             else
461             {
462                 int foregroundColor = colorRegExp.cap(2).toInt();
463                 colorString += "<font color=\"" + Preferences::ircColorCode(foregroundColor).name() + "\">";
464             }
465
466             firstColor = false;
467         }
468
469         filteredLine.replace(pos, colorRegExp.cap(0).length(), colorString);
470     }
471
472     if(!firstColor)
473         filteredLine+="</font>";
474
475     // Replace all text decorations
476     // TODO: \017 should reset all textt decorations to plain text
477     replaceDecoration(filteredLine,'\x02','b');
478     replaceDecoration(filteredLine,'\x09','i');
479     replaceDecoration(filteredLine,'\x13','s');
480     replaceDecoration(filteredLine,'\x15','u');
481     replaceDecoration(filteredLine,'\x16','b');   // should be inverse
482     replaceDecoration(filteredLine,'\x1f','u');
483
484     if(parseURL)
485     {
486         filteredLine = Konversation::tagURLs(filteredLine, whoSent);
487     }
488     else
489     {
490         // Change & to &amp; to prevent html entities to do strange things to the text
491         filteredLine.replace('&', "&amp;");
492         filteredLine.replace("\x0b", "&");
493     }
494
495     filteredLine = Konversation::EmotIcon::filter(filteredLine, fontMetrics());
496
497     // Highlight
498     QString ownNick;
499
500     if (m_server)
501     {
502         ownNick = m_server->getNickname();
503     }
504     else if (m_chatWin->getType() == ChatWindow::DccChat)
505     {
506         ownNick = static_cast<DccChat*>(m_chatWin)->getOwnNick();
507     }
508
509     if(doHighlight && (whoSent != ownNick) && !self)
510     {
511         QString highlightColor;
512
513         if(Preferences::highlightNick() &&
514             filteredLine.lower().find(QRegExp("(^|[^\\d\\w])" +
515             QRegExp::escape(ownNick.lower()) +
516             "([^\\d\\w]|$)")) != -1)
517         {
518             // highlight current nickname
519             highlightColor = Preferences::highlightNickColor().name();
520             m_tabNotification = Konversation::tnfNick;
521         }
522         else
523         {
524             QPtrList<Highlight> highlightList = Preferences::highlightList();
525             QPtrListIterator<Highlight> it(highlightList);
526             Highlight* highlight = it.current();
527             bool patternFound = false;
528             int index = 0;
529
530             QStringList captures;
531             while(highlight)
532             {
533                 if(highlight->getRegExp())
534                 {
535                     QRegExp needleReg=highlight->getPattern();
536                     needleReg.setCaseSensitive(false);
537                                                   // highlight regexp in text
538                     patternFound = ((filteredLine.find(needleReg) != -1) ||
539                                                   // highlight regexp in nickname
540                         (whoSent.find(needleReg) != -1));
541
542                     // remember captured patterns for later
543                     captures=needleReg.capturedTexts();
544
545                 }
546                 else
547                 {
548                     QString needle=highlight->getPattern();
549                                                   // highlight patterns in text
550                     patternFound = ((filteredLine.find(needle, 0, false) != -1) ||
551                                                   // highlight patterns in nickname
552                         (whoSent.find(needle, 0, false) != -1));
553                 }
554
555                 if(!patternFound)
556                 {
557                     ++it;
558                     highlight = it.current();
559                     ++index;
560                 }
561                 else
562                 {
563                     break;
564                 }
565             }
566
567             if(patternFound)
568             {
569                 highlightColor = highlight->getColor().name();
570                 m_highlightColor = highlightColor;
571                 m_tabNotification = Konversation::tnfHighlight;
572
573                 if(Preferences::highlightSoundsEnabled() && m_chatWin->notificationsEnabled())
574                 {
575                     konvApp->sound()->play(highlight->getSoundURL());
576                 }
577
578                 konvApp->notificationHandler()->highlight(m_chatWin, whoSent, line);
579                 m_autoTextToSend = highlight->getAutoText();
580
581                 // replace %0 - %9 in regex groups
582                 for(unsigned int capture=0;capture<captures.count();capture++)
583                 {
584                   m_autoTextToSend.replace(QString("%%1").arg(capture),captures[capture]);
585                 }
586                 m_autoTextToSend.replace(QRegExp("%[0-9]"),QString());
587             }
588         }
589
590         // apply found highlight color to line
591         if(!highlightColor.isEmpty())
592         {
593             filteredLine = "<font color=\"" + highlightColor + "\">" + filteredLine + "</font>";
594         }
595     }
596     else if(doHighlight && (whoSent == ownNick) && Preferences::highlightOwnLines())
597     {
598         // highlight own lines
599         filteredLine = "<font color=\"" + Preferences::highlightOwnLinesColor().name() +
600             "\">" + filteredLine + "</font>";
601     }
602
603     // Replace pairs of spaces with "<space>&nbsp;" to preserve some semblance of text wrapping
604     filteredLine.replace("  "," \xA0");
605     return filteredLine;
606 }
607
608 QString IRCView::createNickLine(const QString& nick, bool encapsulateNick, bool privMsg)
609 {
610     QString nickLine = "%2";
611
612     if(Preferences::useClickableNicks())
613     {
614         // HACK:Use space as a placeholder for \ as Qt tries to be clever and does a replace to / in urls in QTextEdit
615         nickLine = "<a href=\"#" + QString(nick).replace('\\', " ") + "\">%2</a>";
616     }
617
618     if(privMsg)
619     {
620         nickLine.prepend ("-&gt; ");
621     }
622
623     if(encapsulateNick)
624         nickLine = "&lt;" + nickLine + "&gt;";
625
626     if(Preferences::useColoredNicks() && m_server)
627     {
628         QString nickColor;
629
630         if (nick != m_server->getNickname())
631             nickColor = Preferences::nickColor(m_server->obtainNickInfo(nick)->getNickColor()).name();
632         else
633             nickColor =  Preferences::nickColor(8).name();
634
635         if(nickColor == "#000000")
636         {
637             nickColor = "#000001";                    // HACK Working around QTextBrowser's auto link coloring
638         }
639
640         nickLine = "<font color=\"" + nickColor + "\">"+nickLine+"</font>";
641     }
642     //FIXME: Another last-minute hack to get DCC Chat colored nicknames
643     // working. We can't use NickInfo::getNickColor() because we don't
644     // have a server.
645     else if (Preferences::useColoredNicks() && m_chatWin->getType() == ChatWindow::DccChat)
646     {
647         QString ownNick = static_cast<DccChat*>(m_chatWin)->getOwnNick();
648         QString nickColor;
649
650         if (nick != ownNick)
651         {
652             int nickvalue = 0;
653
654             for (uint index = 0; index < nick.length(); index++)
655             {
656                 nickvalue += nick[index].unicode();
657             }
658
659             nickColor = Preferences::nickColor((nickvalue % 8)).name();
660         }
661         else
662             nickColor =  Preferences::nickColor(8).name();
663
664         if(nickColor == "#000000")
665         {
666             nickColor = "#000001";                    // HACK Working around QTextBrowser's auto link coloring
667         }
668
669         nickLine = "<font color=\"" + nickColor + "\">"+nickLine+"</font>";
670     }
671
672     if(Preferences::useBoldNicks())
673         nickLine = "<b>" + nickLine + "</b>";
674
675     return nickLine;
676 }
677
678 void IRCView::append(const QString& nick,const QString& message)
679 {
680     QString channelColor = Preferences::color(Preferences::ChannelMessage).name();
681
682     if(channelColor  == "#000000")
683     {
684         channelColor = "#000001";              // HACK Working around QTextBrowser's auto link coloring
685     }
686
687     QString line;
688     m_tabNotification = Konversation::tnfNormal;
689
690     QString nickLine = createNickLine(nick);
691
692     if(basicDirection(message) == QChar::DirR)
693     {
694         line = RLE;
695         line += LRE;
696         line += "<p><font color=\"" + channelColor + "\">" + nickLine + " %1" + PDF + RLM + " %3</font></p>\n";
697     }
698     else
699     {
700         line = "<p><font color=\"" + channelColor + "\">%1" + nickLine + " %3</font></p>\n";
701     }
702
703     line = line.arg(timeStamp(), nick, filter(message, channelColor, nick, true));
704
705     emit textToLog(QString("<%1>\t%2").arg(nick).arg(message));
706
707     doAppend(line);
708
709     m_lastInsertionWasLine = false;
710 }
711
712 void IRCView::appendLine()
713 {
714     if (m_lastInsertionWasLine) return;
715
716     QColor channelColor=Preferences::color(Preferences::ChannelMessage);
717     m_tabNotification = Konversation::tnfNone;
718
719     QString line = "<p><font color=\"" + channelColor.name() + "\"><br><hr color=\""+Preferences::color(Preferences::CommandMessage).name()+"\" noshade></font></p>\n";
720
721     doAppend(line);
722
723     m_lastInsertionWasLine = true;
724 }
725
726 void IRCView::appendRaw(const QString& message, bool suppressTimestamps, bool self)
727 {
728     QColor channelColor=Preferences::color(Preferences::ChannelMessage);
729     QString line;
730     m_tabNotification = Konversation::tnfNone;
731
732     if(suppressTimestamps)
733     {
734         line = QString("<p><font color=\"" + channelColor.name() + "\">" + message + "</font></p>\n");
735     }
736     else
737     {
738         line = QString("<p>" + timeStamp() + " <font color=\"" + channelColor.name() + "\">" + message + "</font></p>\n");
739     }
740
741     doAppend(line, self);
742
743     m_lastInsertionWasLine = false;
744 }
745
746 void IRCView::appendQuery(const QString& nick, const QString& message, bool inChannel)
747 {
748     QString queryColor=Preferences::color(Preferences::QueryMessage).name();
749
750     if(queryColor  == "#000000")
751     {
752         queryColor = "#000001";                // HACK Working around QTextBrowser's auto link coloring
753     }
754
755     QString line;
756     m_tabNotification = Konversation::tnfPrivate;
757
758     QString nickLine = createNickLine(nick, true, inChannel);
759
760     if(basicDirection(message) == QChar::DirR)
761     {
762         line = RLE;
763         line += LRE;
764         line += "<p><font color=\"" + queryColor + "\">" + nickLine + " %1" + PDF + " %3</font></p>\n";
765     }
766     else
767     {
768         line = "<p><font color=\"" + queryColor + "\">%1 " + nickLine + " %3</font></p>\n";
769     }
770
771     line = line.arg(timeStamp(), nick, filter(message, queryColor, nick, true));
772
773     emit textToLog(QString("<%1>\t%2").arg(nick).arg(message));
774
775     doAppend(line);
776
777     m_lastInsertionWasLine = false;
778 }
779
780 void IRCView::appendAction(const QString& nick,const QString& message)
781 {
782     QString actionColor=Preferences::color(Preferences::ActionMessage).name();
783
784     if(actionColor  == "#000000")
785     {
786         actionColor = "#000001";               // HACK Working around QTextBrowser's auto link coloring
787     }
788
789     QString line;
790     m_tabNotification = Konversation::tnfNormal;
791
792     QString nickLine = createNickLine(nick, false);
793
794     if(basicDirection(message) == QChar::DirR)
795     {
796         line = RLE;
797         line += LRE;
798         line += "<p><font color=\"" + actionColor + "\">" + nickLine + " * %1" + PDF + " %3</font></p>\n";
799     }
800     else
801     {
802         line = "<p><font color=\"" + actionColor + "\">%1 * " + nickLine + " %3</font></p>\n";
803     }
804
805     line = line.arg(timeStamp(), nick, filter(message, actionColor, nick, true));
806
807     emit textToLog(QString("\t * %1 %2").arg(nick).arg(message));
808
809     doAppend(line);
810
811     m_lastInsertionWasLine = false;
812 }
813
814 void IRCView::appendServerMessage(const QString& type, const QString& message, bool parseURL)
815 {
816     QString serverColor = Preferences::color(Preferences::ServerMessage).name();
817     m_tabNotification = Konversation::tnfControl;
818
819     // Fixed width font option for MOTD
820     QString fixed;
821     if(Preferences::fixedMOTD() && !m_fontDataBase.isFixedPitch(font().family()))
822     {
823         if(type == i18n("MOTD"))
824             fixed=" face=\"" + KGlobalSettings::fixedFont().family() + "\"";
825     }
826
827     QString line;
828
829     if(basicDirection(message) == QChar::DirR)
830     {
831         line = RLE;
832         line += LRE;
833         line += "<p><font color=\"" + serverColor + "\"" + fixed + "><b>[</b>%2<b>]</b> %1" + PDF + " %3</font></p>\n";
834     }
835     else
836     {
837         line = "<p><font color=\"" + serverColor + "\"" + fixed + ">%1 <b>[</b>%2<b>]</b> %3</font></p>\n";
838     }
839
840     if(type != i18n("Notify"))
841         line = line.arg(timeStamp(), type, filter(message, serverColor, 0 , true, parseURL));
842     else
843         line = "<font color=\"" + serverColor + "\">"+line.arg(timeStamp(), type, message)+"</font>";
844
845     emit textToLog(QString("%1\t%2").arg(type).arg(message));
846
847     doAppend(line);
848
849     m_lastInsertionWasLine = false;
850 }
851
852 void IRCView::appendCommandMessage(const QString& type,const QString& message, bool important, bool parseURL, bool self)
853 {
854     if (Preferences::hideUnimportantEvents() && !important)
855         return;
856
857     QString commandColor = Preferences::color(Preferences::CommandMessage).name();
858     QString line;
859     QString prefix="***";
860     m_tabNotification = Konversation::tnfControl;
861
862     if(type == i18n("Join"))
863     {
864         prefix="-->";
865         parseURL=false;
866     }
867     else if(type == i18n("Part") || type == i18n("Quit"))
868     {
869         prefix="<--";
870     }
871
872     prefix=QStyleSheet::escape(prefix);
873
874     if(basicDirection(message) == QChar::DirR)
875     {
876         line = RLE;
877         line += LRE;
878         line += "<p><font color=\"" + commandColor + "\">%2 %1" + PDF + " %3</font></p>\n";
879     }
880     else
881     {
882         line = "<p><font color=\"" + commandColor + "\">%1 %2 %3</font></p>\n";
883     }
884
885     line = line.arg(timeStamp(), prefix, filter(message, commandColor, 0, true, parseURL, self));
886
887     emit textToLog(QString("%1\t%2").arg(type).arg(message));
888
889     doAppend(line, self);
890
891     m_lastInsertionWasLine = false;
892 }
893
894 void IRCView::appendBacklogMessage(const QString& firstColumn,const QString& rawMessage)
895 {
896     QString time;
897     QString message = rawMessage;
898     QString nick = firstColumn;
899     QString backlogColor = Preferences::color(Preferences::BacklogMessage).name();
900     m_tabNotification = Konversation::tnfNone;
901
902     time = nick.section(' ', 0, 4);
903     nick = nick.section(' ', 5);
904
905     if(!nick.isEmpty() && !nick.startsWith("<") && !nick.startsWith("*"))
906     {
907         nick = '|' + nick + '|';
908     }
909
910     // Nicks are in "<nick>" format so replace the "<>"
911     nick.replace("<","&lt;");
912     nick.replace(">","&gt;");
913
914     QString line;
915
916     if(basicDirection(message) == QChar::DirR)
917     {
918         line = "<p><font color=\"" + backlogColor + "\">%2 %1 %3</font></p>\n";
919     }
920     else
921     {
922         line = "<p><font color=\"" + backlogColor + "\">%1 %2 %3</font></p>\n";
923     }
924
925     line = line.arg(time, nick, filter(message, backlogColor, NULL, false, false));
926
927     m_lastInsertionWasLine = false;
928
929     doAppend(line);
930 }
931
932 //without any display update stuff that freaks out the scrollview
933 void IRCView::removeSelectedText( int selNum )
934 {
935     QTextDocument* doc=document();
936
937     for ( int i = 0; i < (int)doc->numSelections(); ++i )
938     {
939         if ( i == selNum )
940             continue;
941         doc->removeSelection( i );
942     }
943     // ...snip...
944
945     doc->removeSelectedText( selNum, QTextEdit::textCursor() );
946
947     // ...snip...
948 }
949
950 void IRCView::scrollToBottom()
951 {
952     // QTextEdit::scrollToBottom does sync() too, but we don't want it because its slow
953     setContentsPos( contentsX(), contentsHeight() - visibleHeight() );
954 }
955
956 void IRCView::doAppend(const QString& newLine, bool self)
957 {
958     // Add line to buffer
959     QString line(newLine);
960
961     if (!self) emit updateTabNotification(m_tabNotification);
962
963     // scroll view only if the scroll bar is already at the bottom
964     bool doScroll = ( KTextBrowser::verticalScrollBar()->value() == KTextBrowser::verticalScrollBar()->maxValue());
965
966     line.remove('\n');                        // TODO why have newlines? we get <p>, so the \n are unnecessary...
967
968     bool up = KTextBrowser::viewport()->isUpdatesEnabled();
969
970     KTextBrowser::viewport()->setUpdatesEnabled(false);
971     int paraFrom, indexFrom, paraTo, indexTo;
972     bool textselected = hasSelectedText();
973
974     // Remember the selection so we don't loose it when adding the new line
975     if(textselected)
976         getSelection(&paraFrom, &indexFrom, &paraTo, &indexTo);
977
978     KTextBrowser::append(line);
979
980     document()->lastParagraph()->format();
981
982     // get maximum number of lines we want to have in the scollback buffer
983     int sbm = Preferences::scrollbackMax();
984
985     // Explanation: the scrolling mechanism cannot handle the buffer changing when the scrollbar is not
986     //              at an end, so the scrollbar wets its pants and forgets who it is for ten minutes
987     // Also make sure not to delete any lines if maximum lines of scrollback is set to 0 (unlimited)
988     if (sbm && doScroll)
989     {
990         int numRemoved = paragraphs() - sbm;
991
992         if(numRemoved >0)
993         {
994             for (int index = numRemoved; index > 0; --index)
995             {
996                 removeParagraph(0);
997             }
998
999             if(textselected)
1000             {
1001                 paraFrom -= numRemoved;
1002                 paraTo -= numRemoved;
1003             }
1004         }
1005     }
1006
1007     resizeContents(contentsWidth(), document()->height());
1008     KTextBrowser::viewport()->setUpdatesEnabled(up);
1009
1010     // Restore selection
1011     if(textselected && paraFrom >= 0 && paraTo >= 0)
1012         setSelection(paraFrom, indexFrom, paraTo, indexTo);
1013
1014     if (doScroll) updateScrollBarPos();
1015
1016     //FIXME: Disable auto-text for DCC Chats since we don't have a server
1017     // to parse wildcards.
1018     if (!m_autoTextToSend.isEmpty() && m_server)
1019     {
1020         // replace placeholders in autoText
1021         QString sendText = m_server->parseWildcards(m_autoTextToSend,m_server->getNickname(),
1022             QString(), QString(), QString(), QString());
1023         // avoid recursion due to signalling
1024         m_autoTextToSend = QString();
1025         // send signal only now
1026         emit autoText(sendText);
1027     }
1028     else
1029     {
1030         m_autoTextToSend = QString();
1031     }
1032
1033     if (!m_lastStatusText.isEmpty()) emit clearStatusBarTempText();
1034 }
1035
1036 // remember if scrollbar was positioned at the end of the text or not
1037 void IRCView::hideEvent(QHideEvent* /* event */) {
1038     m_resetScrollbar = ((contentsHeight()-visibleHeight()) == contentsY());
1039 }
1040
1041 // Workaround to scroll to the end of the TextView when it's shown
1042 void IRCView::showEvent(QShowEvent* event) {
1043     Q_UNUSED(event);
1044     // did the user scroll the view to the end of the text before hiding?
1045     if(m_resetScrollbar)
1046     {
1047         moveCursor(MoveEnd,false);
1048         ensureVisible(0,contentsHeight());
1049     }
1050 }
1051
1052 void IRCView::contentsMouseReleaseEvent(QMouseEvent *ev)
1053 {
1054     if (ev->button() == Qt::MidButton)
1055     {
1056         if(m_copyUrlMenu)
1057         {
1058             openLink(m_urlToCopy,true);
1059             return;
1060         }
1061         else
1062         {
1063             emit textPasted(true);
1064             return;
1065         }
1066     }
1067
1068     if (ev->button() == QMouseEvent::LeftButton)
1069     {
1070         if (m_mousePressed)
1071         {
1072             if (ev->state() == (Qt::LeftButton|Qt::ShiftButton))
1073                 saveLinkAs(m_highlightedURL);
1074             else
1075                 openLink(m_highlightedURL);
1076
1077             m_mousePressed = false;
1078             return;
1079         }
1080     }
1081
1082     KTextBrowser::contentsMouseReleaseEvent(ev);
1083 }
1084
1085 void IRCView::contentsMousePressEvent(QMouseEvent* ev)
1086 {
1087     if (ev->button() == QMouseEvent::LeftButton)
1088     {
1089         m_urlToDrag = m_highlightedURL;
1090
1091         if (!m_urlToDrag.isNull())
1092         {
1093             m_mousePressed = true;
1094             m_pressPosition = ev->pos();
1095             return;
1096         }
1097     }
1098
1099     KTextBrowser::contentsMousePressEvent(ev);
1100 }
1101
1102 void IRCView::contentsMouseMoveEvent(QMouseEvent* ev)
1103 {
1104     if (m_mousePressed && (m_pressPosition - ev->pos()).manhattanLength() > QApplication::startDragDistance())
1105     {
1106         m_mousePressed = false;
1107         removeSelection();
1108         KURL ux = KURL::fromPathOrURL(m_urlToDrag);
1109
1110         if (m_server && m_urlToDrag.startsWith("##"))
1111         {
1112             ux = QString("irc://%1:%2/%3").arg(m_server->getServerName()).arg(m_server->getPort()).arg(m_urlToDrag.mid(2));
1113         }
1114         else if (m_urlToDrag.startsWith("#"))
1115         {
1116             ux = m_urlToDrag.mid(1);
1117         }
1118
1119         KURLDrag* u = new KURLDrag(ux, viewport());
1120         u->drag();
1121         return;
1122     }
1123
1124     KTextBrowser::contentsMouseMoveEvent(ev);
1125 }
1126
1127 void IRCView::contentsContextMenuEvent(QContextMenuEvent* ev)
1128 {
1129     bool block = contextMenu(ev);
1130
1131     // HACK Replace % with \x03 in the url to keep Qt from doing stupid things
1132     m_highlightedURL = anchorAt(viewportToContents(mapFromGlobal(QCursor::pos())));
1133     m_highlightedURL = m_highlightedURL.replace('\x03', "%");
1134     // Hack to counter the fact that we're given an decoded url
1135     m_highlightedURL = KURL::fromPathOrURL(m_highlightedURL).url();
1136
1137     if (m_highlightedURL.isEmpty()) viewport()->setCursor(Qt::ArrowCursor);
1138
1139     if(m_highlightedURL.startsWith("#"))
1140     {
1141         // HACK:Use space as a placeholder for \ as Qt tries to be clever and does a replace to / in urls in QTextEdit
1142         m_highlightedURL = m_highlightedURL.replace(' ', "\\");
1143     }
1144
1145     if (!block)
1146         KTextBrowser::contentsContextMenuEvent(ev);
1147 }
1148
1149 bool IRCView::contextMenu(QContextMenuEvent* ce)
1150 {
1151     if (m_server && m_isOnNick && m_nickPopup->isEnabled())
1152     {
1153         updateNickMenuEntries(m_nickPopup, getContextNick());
1154
1155         if(m_nickPopup->exec(ce->globalPos()) == -1)
1156             clearContextNick();
1157
1158         m_isOnNick = false;
1159     }
1160     else if (m_server && m_isOnChannel && m_channelPopup->isEnabled())
1161     {
1162         m_channelPopup->exec(ce->globalPos());
1163         m_isOnChannel = false;
1164     }
1165     else
1166     {
1167         KActionCollection* actionCollection = KonversationApplication::instance()->getMainWindow()->actionCollection();
1168         KToggleAction* toggleMenuBarAction = static_cast<KToggleAction*>(actionCollection->action("options_show_menubar"));
1169
1170         if (toggleMenuBarAction && !toggleMenuBarAction->isChecked())
1171         {
1172             toggleMenuBarAction->plug(m_popup, 0);
1173             m_popup->setItemVisible(toggleMenuBarSeparator, true);
1174         }
1175
1176         m_popup->setItemEnabled(Copy,(hasSelectedText()));
1177
1178         KAction* channelSettingsAction = 0;
1179
1180         if (m_chatWin->getType() == ChatWindow::Channel)
1181         {
1182             channelSettingsAction = KonversationApplication::instance()->getMainWindow()->actionCollection()->action("channel_settings");
1183             if (channelSettingsAction) channelSettingsAction->plug(m_popup);
1184         }
1185
1186         if (m_chatWin->getType() == ChatWindow::Query)
1187             updateNickMenuEntries(m_popup, m_chatWin->getName());
1188
1189         int r = m_popup->exec(ce->globalPos());
1190
1191         switch (r)
1192         {
1193             case -1:
1194                 // dummy. -1 means, no entry selected. we don't want -1to go in default, so
1195                 // we catch it here
1196                 break;
1197             case Copy:
1198                 copy();
1199                 break;
1200             case CopyUrl:
1201             {
1202                 QClipboard *cb = KApplication::kApplication()->clipboard();
1203                 cb->setText(m_urlToCopy,QClipboard::Selection);
1204                 cb->setText(m_urlToCopy,QClipboard::Clipboard);
1205                 break;
1206             }
1207             case SelectAll:
1208                 selectAll();
1209                 break;
1210             case Search:
1211                 search();
1212                 break;
1213             case SendFile:
1214                 emit sendFile();
1215                 break;
1216             case Bookmark:
1217             {
1218                 KBookmarkManager* bm = KBookmarkManager::userBookmarksManager();
1219                 KBookmarkGroup bg = bm->addBookmarkDialog(m_urlToCopy, QString());
1220                 bm->save();
1221                 bm->emitChanged(bg);
1222                 break;
1223             }
1224             case SaveAs:
1225                 saveLinkAs(m_urlToCopy);
1226                 break;
1227             default:
1228                 emit extendedPopup(r);
1229         }
1230
1231         if (toggleMenuBarAction)
1232         {
1233             toggleMenuBarAction->unplug(m_popup);
1234             m_popup->setItemVisible(toggleMenuBarSeparator, false);
1235         }
1236
1237         if (channelSettingsAction) channelSettingsAction->unplug(m_popup);
1238     }
1239     return true;
1240 }
1241
1242 void IRCView::setupNickPopupMenu()
1243 {
1244     m_nickPopup = new KPopupMenu(this,"nicklist_context_menu");
1245     m_modes = new KPopupMenu(this,"nicklist_modes_context_submenu");
1246     m_kickban = new KPopupMenu(this,"nicklist_kick_ban_context_submenu");
1247     m_nickPopupId= m_nickPopup->insertTitle(m_currentNick);
1248
1249     m_nickPopup->insertItem(i18n("&Whois"),Konversation::Whois);
1250     m_nickPopup->insertItem(i18n("&Version"),Konversation::Version);
1251     m_nickPopup->insertItem(i18n("&Ping"),Konversation::Ping);
1252
1253     m_nickPopup->insertSeparator();
1254
1255     m_modes->insertItem(i18n("Give Op"),Konversation::GiveOp);
1256     m_modes->insertItem(i18n("Take Op"),Konversation::TakeOp);
1257     m_modes->insertItem(i18n("Give Voice"),Konversation::GiveVoice);
1258     m_modes->insertItem(i18n("Take Voice"),Konversation::TakeVoice);
1259     m_nickPopup->insertItem(i18n("Modes"),m_modes,Konversation::ModesSub);
1260
1261     m_kickban->insertItem(i18n("Kick"),Konversation::Kick);
1262     m_kickban->insertItem(i18n("Kickban"),Konversation::KickBan);
1263     m_kickban->insertItem(i18n("Ban Nickname"),Konversation::BanNick);
1264     m_kickban->insertSeparator();
1265     m_kickban->insertItem(i18n("Ban *!*@*.host"),Konversation::BanHost);
1266     m_kickban->insertItem(i18n("Ban *!*@domain"),Konversation::BanDomain);
1267     m_kickban->insertItem(i18n("Ban *!user@*.host"),Konversation::BanUserHost);
1268     m_kickban->insertItem(i18n("Ban *!user@domain"),Konversation::BanUserDomain);
1269     m_kickban->insertSeparator();
1270     m_kickban->insertItem(i18n("Kickban *!*@*.host"),Konversation::KickBanHost);
1271     m_kickban->insertItem(i18n("Kickban *!*@domain"),Konversation::KickBanDomain);
1272     m_kickban->insertItem(i18n("Kickban *!user@*.host"),Konversation::KickBanUserHost);
1273     m_kickban->insertItem(i18n("Kickban *!user@domain"),Konversation::KickBanUserDomain);
1274     m_nickPopup->insertItem(i18n("Kick / Ban"),m_kickban,Konversation::KickBanSub);
1275
1276     m_nickPopup->insertItem(i18n("Ignore"), Konversation::IgnoreNick);
1277     m_nickPopup->insertItem(i18n("Unignore"), Konversation::UnignoreNick);
1278     m_nickPopup->setItemVisible(Konversation::IgnoreNick, false);
1279     m_nickPopup->setItemVisible(Konversation::UnignoreNick, false);
1280
1281     m_nickPopup->insertSeparator();
1282
1283     m_nickPopup->insertItem(i18n("Open Query"),Konversation::OpenQuery);
1284     if (kapp->authorize("allow_downloading"))
1285     {
1286         m_nickPopup->insertItem(SmallIcon("2rightarrow"),i18n("Send &File..."),Konversation::DccSend);
1287     }
1288     m_nickPopup->insertSeparator();
1289
1290     m_nickPopup->insertItem(i18n("Add to Watched Nicks"), Konversation::AddNotify);
1291
1292     connect(m_nickPopup, SIGNAL(activated(int)), this, SIGNAL(popupCommand(int)));
1293     connect(m_modes, SIGNAL(activated(int)), this, SIGNAL(popupCommand(int)));
1294     connect(m_kickban, SIGNAL(activated(int)), this, SIGNAL(popupCommand(int)));
1295 }
1296
1297 void IRCView::updateNickMenuEntries(QPopupMenu* popup, const QString& nickname)
1298 {
1299     if (popup)
1300     {
1301         if (Preferences::isIgnored(nickname))
1302         {
1303             popup->setItemVisible(Konversation::UnignoreNick, true);
1304             popup->setItemVisible(Konversation::IgnoreNick, false);
1305         }
1306         else
1307         {
1308             popup->setItemVisible(Konversation::IgnoreNick, true);
1309             popup->setItemVisible(Konversation::UnignoreNick, false);
1310         }
1311
1312         if (!m_server)
1313             popup->setItemEnabled(Konversation::AddNotify, false);
1314         else if (!m_server->isConnected())
1315             popup->setItemEnabled(Konversation::AddNotify, false);
1316         else if (!Preferences::hasNotifyList(m_server->serverGroupSettings()->id()))
1317             popup->setItemEnabled(Konversation::AddNotify, false);
1318         else if (Preferences::isNotify(m_server->serverGroupSettings()->id(), nickname))
1319             popup->setItemEnabled(Konversation::AddNotify, false);
1320         else
1321             popup->setItemEnabled(Konversation::AddNotify, true);
1322     }
1323 }
1324
1325 void IRCView::setupQueryPopupMenu()
1326 {
1327     m_nickPopup = new KPopupMenu(this,"query_context_menu");
1328     m_nickPopupId = m_nickPopup->insertTitle(m_currentNick);
1329     m_nickPopup->insertItem(i18n("&Whois"),Konversation::Whois);
1330     m_nickPopup->insertItem(i18n("&Version"),Konversation::Version);
1331     m_nickPopup->insertItem(i18n("&Ping"),Konversation::Ping);
1332     m_nickPopup->insertSeparator();
1333
1334     m_nickPopup->insertItem(i18n("Ignore"), Konversation::IgnoreNick);
1335     m_nickPopup->insertItem(i18n("Unignore"), Konversation::UnignoreNick);
1336     m_nickPopup->setItemVisible(Konversation::IgnoreNick, false);
1337     m_nickPopup->setItemVisible(Konversation::UnignoreNick, false);
1338     m_nickPopup->insertSeparator();
1339
1340     if (kapp->authorize("allow_downloading"))
1341     {
1342         m_nickPopup->insertItem(SmallIcon("2rightarrow"),i18n("Send &File..."),Konversation::DccSend);
1343         m_nickPopup->insertSeparator();
1344     }
1345
1346     m_nickPopup->insertItem(i18n("Add to Watched Nicks"), Konversation::AddNotify);
1347
1348     connect(m_nickPopup, SIGNAL(activated(int)), this, SIGNAL(popupCommand(int)));
1349 }
1350
1351 void IRCView::setupChannelPopupMenu()
1352 {
1353     m_channelPopup = new KPopupMenu(this,"channel_context_menu");
1354     m_channelPopupId = m_channelPopup->insertTitle(m_currentChannel);
1355     m_channelPopup->insertItem(i18n("&Join"),Konversation::Join);
1356     m_channelPopup->insertItem(i18n("Get &user list"),Konversation::Names);
1357     m_channelPopup->insertItem(i18n("Get &topic"),Konversation::Topic);
1358
1359     connect(m_channelPopup, SIGNAL(activated(int)), this, SIGNAL(popupCommand(int)));
1360 }
1361
1362 void IRCView::setNickAndChannelContextMenusEnabled(bool enable)
1363 {
1364     if (m_nickPopup) m_nickPopup->setEnabled(enable);
1365     if (m_channelPopup) m_channelPopup->setEnabled(enable);
1366 }
1367
1368 void IRCView::search()
1369 {
1370     emit doSearch();
1371 }
1372
1373 void IRCView::searchAgain()
1374 {
1375     if (m_pattern.isEmpty())
1376     {
1377         emit doSearch();
1378     }
1379     else
1380     {
1381         // next search must begin one index before / after the last search
1382         // depending on the search direction.
1383         if(m_forward)
1384         {
1385             ++m_findIndex;
1386             if(m_findIndex == paragraphLength(m_findParagraph))
1387             {
1388                 m_findIndex = 0;
1389                 ++m_findParagraph;
1390             }
1391         }
1392         else
1393         {
1394             if(m_findIndex)
1395             {
1396                 --m_findIndex;
1397             }
1398             else
1399             {
1400                 --m_findParagraph;
1401                 m_findIndex = paragraphLength(m_findParagraph);
1402             }
1403         }
1404
1405         if(!find(m_pattern, m_caseSensitive, m_wholeWords, m_forward, &m_findParagraph, &m_findIndex))
1406         {
1407             KMessageBox::information(this,i18n("No matches found for \"%1\".").arg(m_pattern),i18n("Information"));
1408         }
1409
1410     }
1411 }
1412
1413 bool IRCView::search(const QString& pattern, bool caseSensitive,
1414 bool wholeWords, bool forward, bool fromCursor)
1415 {
1416     m_pattern       = pattern;
1417     m_caseSensitive = caseSensitive;
1418     m_wholeWords    = wholeWords;
1419     m_forward       = forward;
1420     m_fromCursor    = fromCursor;
1421
1422     if (m_pattern.isEmpty())
1423         return true;
1424
1425     if (!m_fromCursor)
1426     {
1427         if(m_forward)
1428         {
1429             m_findParagraph = 1;
1430             m_findIndex = 1;
1431         }
1432         else
1433         {
1434             m_findParagraph = paragraphs();
1435             m_findIndex = paragraphLength(paragraphs());
1436         }
1437     }
1438
1439     return searchNext();
1440 }
1441
1442 bool IRCView::searchNext(bool reversed)
1443 {
1444     if (m_pattern.isEmpty())
1445         return true;
1446
1447     bool fwd = (reversed ? !m_forward : m_forward);
1448     // next search must begin one index before / after the last search
1449     // depending on the search direction.
1450     if (fwd)
1451     {
1452         ++m_findIndex;
1453         if(m_findIndex == paragraphLength(m_findParagraph))
1454         {
1455             m_findIndex = 0;
1456             ++m_findParagraph;
1457         }
1458     }
1459     else
1460     {
1461         if (m_findIndex)
1462         {
1463             --m_findIndex;
1464         }
1465         else
1466         {
1467             --m_findParagraph;
1468             m_findIndex = paragraphLength(m_findParagraph);
1469         }
1470     }
1471
1472     return find(m_pattern, m_caseSensitive, m_wholeWords, fwd,
1473         &m_findParagraph, &m_findIndex);
1474 }
1475
1476 // other windows can link own menu entries here
1477 QPopupMenu* IRCView::getPopup() const
1478 {
1479     return m_popup;
1480 }
1481
1482 // for more information about these RTFM
1483 //    http://www.unicode.org/reports/tr9/
1484 //    http://www.w3.org/TR/unicode-xml/
1485 QChar IRCView::LRM = (ushort)0x200e; // Right-to-Left Mark
1486 QChar IRCView::RLM = (ushort)0x200f; // Left-to-Right Mark
1487 QChar IRCView::LRE = (ushort)0x202a; // Left-to-Right Embedding
1488 QChar IRCView::RLE = (ushort)0x202b; // Right-to-Left Embedding
1489 QChar IRCView::RLO = (ushort)0x202e; // Right-to-Left Override
1490 QChar IRCView::LRO = (ushort)0x202d; // Left-to-Right Override
1491 QChar IRCView::PDF = (ushort)0x202c; // Previously Defined Format
1492
1493 QChar::Direction IRCView::basicDirection(const QString &string)
1494 {
1495     // The following code decides between LTR or RTL direction for
1496     // a line based on the amount of each type of characters pre-
1497     // sent. It does so by counting, but stops when one of the two
1498     // counters becomes higher than half of the string length to
1499     // avoid unnecessary work.
1500
1501     unsigned int pos = 0;
1502     unsigned int rtl_chars = 0;
1503     unsigned int ltr_chars = 0;
1504     unsigned int str_len = string.length();
1505     unsigned int str_half_len = str_len/2;
1506
1507     for(pos=0; pos < str_len; pos++)
1508     {
1509         if (!(string[pos].isNumber() || string[pos].isSymbol() ||
1510             string[pos].isSpace()  || string[pos].isPunct()  ||
1511             string[pos].isMark()))
1512         {
1513             switch(string[pos].direction())
1514             {
1515                 case QChar::DirL:
1516                 case QChar::DirLRO:
1517                 case QChar::DirLRE:
1518                     ltr_chars++;
1519                     break;
1520                 case QChar::DirR:
1521                 case QChar::DirAL:
1522                 case QChar::DirRLO:
1523                 case QChar::DirRLE:
1524                     rtl_chars++;
1525                     break;
1526                 default:
1527                     break;
1528             }
1529         }
1530
1531         if (ltr_chars > str_half_len)
1532             return QChar::DirL;
1533         else if (rtl_chars > str_half_len)
1534             return QChar::DirR;
1535     }
1536
1537     if (rtl_chars > ltr_chars)
1538         return QChar::DirR;
1539     else
1540         return QChar::DirL;
1541 }
1542
1543 void IRCView::contentsDragMoveEvent(QDragMoveEvent *e)
1544 {
1545     if(acceptDrops() && QUriDrag::canDecode(e))
1546         e->accept();
1547 }
1548
1549 void IRCView::contentsDropEvent(QDropEvent *e)
1550 {
1551     QStrList s;
1552     if(QUriDrag::decode(e,s))
1553         emit filesDropped(s);
1554 }
1555
1556 QString IRCView::timeStamp()
1557 {
1558     if(Preferences::timestamping())
1559     {
1560         QTime time = QTime::currentTime();
1561         QString timeColor = Preferences::color(Preferences::Time).name();
1562         QString timeFormat = Preferences::timestampFormat();
1563         QString timeString;
1564
1565         if(!Preferences::showDate())
1566         {
1567             timeString = QString("<font color=\"" + timeColor + "\">[%1]</font> ").arg(time.toString(timeFormat));
1568         }
1569         else
1570         {
1571             QDate date = QDate::currentDate();
1572             timeString = QString("<font color=\"" +
1573                 timeColor + "\">[%1 %2]</font> ")
1574                     .arg(KGlobal::locale()->formatDate(date, true /*short format*/),
1575                          time.toString(timeFormat));
1576         }
1577
1578         return timeString;
1579     }
1580
1581     return QString();
1582 }
1583
1584 void IRCView::setChatWin(ChatWindow* chatWin)
1585 {
1586     m_chatWin = chatWin;
1587
1588     if(m_chatWin->getType()==ChatWindow::Channel)
1589         setupNickPopupMenu();
1590     else
1591         setupQueryPopupMenu();
1592
1593     setupChannelPopupMenu();
1594 }
1595
1596 void IRCView::keyPressEvent(QKeyEvent* e)
1597 {
1598     KKey key(e);
1599
1600     if (KStdAccel::copy().contains(key))
1601     {
1602         copy();
1603         e->accept();
1604         return;
1605     }
1606     else if (KStdAccel::paste().contains(key))
1607     {
1608         emit textPasted(false);
1609         e->accept();
1610         return;
1611     }
1612
1613     KTextBrowser::keyPressEvent(e);
1614 }
1615
1616 void IRCView::resizeEvent(QResizeEvent* e)
1617 {
1618     bool doScroll = ( KTextBrowser::verticalScrollBar()->value() == KTextBrowser::verticalScrollBar()->maxValue());
1619     KTextBrowser::resizeEvent(e);
1620
1621     if(doScroll)
1622     {
1623         QTimer::singleShot(0, this, SLOT(updateScrollBarPos()));
1624     }
1625 }
1626
1627 void IRCView::updateScrollBarPos()
1628 {
1629     ensureVisible(contentsX(), contentsHeight());
1630     repaintContents(false);
1631 }
1632
1633 void IRCView::saveLinkAs(const QString& url)
1634 {
1635     KURL source(url);
1636     KFileDialog dialog(":SaveLinkAs", QString (), this, "savelinkdia", true);
1637     dialog.setCaption(i18n("Save Link As"));
1638     dialog.setSelection(source.fileName());
1639
1640     if(dialog.exec() == QDialog::Rejected)
1641         return;
1642
1643     KURL destination = dialog.selectedURL();
1644     KIO::copyAs(source, destination);
1645 }
1646
1647 #include "ircview.moc"
1648
1649 // kate: space-indent on; tab-width 4; indent-width 4; mixed-indent off; replace-tabs on;
1650 // vim: set et sw=4 ts=4 cino=l1,cs,U1: