/* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ /* Copyright (C) 2002 Dario Abatianni Copyright (C) 2005-2007 Peter Simonsson Copyright (C) 2006-2008 Eike Hein */ #include "ircview.h" #include "channel.h" #include "dccchat.h" #include "konversationapplication.h" #include "konversationmainwindow.h" #include "viewcontainer.h" #include "connectionmanager.h" #include "highlight.h" #include "server.h" #include "konversationsound.h" #include "common.h" #include "emoticon.h" #include "notificationhandler.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include IRCView::IRCView(TQWidget* parent, Server* newServer) : KTextBrowser(parent) { m_copyUrlMenu = false; m_resetScrollbar = true; m_offset = 0; m_mousePressed = false; m_isOnNick = false; m_isOnChannel = false; m_chatWin = 0; m_findParagraph=0; m_findIndex=0; m_nickPopup = 0; m_channelPopup = 0; m_rememberLineParagraph = -1; m_rememberLineDirtyBit = false; m_disableEnsureCursorVisible = false; m_wasPainted = false; setAutoFormatting(TQTextEdit::AutoNone); setUndoRedoEnabled(0); setLinkUnderline(false); setVScrollBarMode(AlwaysOn); setHScrollBarMode(AlwaysOff); setWrapPolicy(TQTextEdit::AtWordOrDocumentBoundary); setNotifyClick(true); setFocusPolicy(TQWidget::ClickFocus); // set basic style sheet for

to make paragraph spacing possible TQStyleSheet* sheet=new TQStyleSheet(this,"ircview_style_sheet"); new TQStyleSheetItem(sheet,"p"); setStyleSheet(sheet); m_popup = new TQPopupMenu(this,"ircview_context_menu"); toggleMenuBarSeparator = m_popup->insertSeparator(); m_popup->setItemVisible(toggleMenuBarSeparator, false); copyUrlMenuSeparator = m_popup->insertSeparator(); m_popup->setItemVisible(copyUrlMenuSeparator, false); m_popup->insertItem(SmallIconSet("edit-copy"),i18n("&Copy"),Copy); m_popup->insertItem(i18n("Select All"),SelectAll); m_popup->insertItem(SmallIcon("edit-find"),i18n("Find Text..."),Search); setServer(newServer); setViewBackground(Preferences::color(Preferences::TextViewBackground),TQString()); if (Preferences::customTextFont()) setFont(Preferences::textFont()); else setFont(TDEGlobalSettings::generalFont()); if (Preferences::useParagraphSpacing()) enableParagraphSpacing(); connect(this, TQ_SIGNAL(highlighted(const TQString&)), this, TQ_SLOT(highlightedSlot(const TQString&))); } IRCView::~IRCView() { delete m_popup; } void IRCView::enableParagraphSpacing() { // Set style sheet for

to define paragraph spacing. TQStyleSheet* sheet = styleSheet(); if (!sheet) return; TQStyleSheetItem* style = sheet->item("p"); if (!style) { kdDebug() << "IRCView::updateStyleSheet(): style == 0!" << endl; return; } style->setDisplayMode(TQStyleSheetItem::DisplayBlock); style->setMargin(TQStyleSheetItem::MarginVertical, Preferences::paragraphSpacing()); style->setSelfNesting(false); } void IRCView::setViewBackground(const TQColor& backgroundColor, const TQString& pixmapName) { TQPixmap backgroundPixmap; backgroundPixmap.load(pixmapName); if(backgroundPixmap.isNull()) { setPaper(backgroundColor); } else { TQBrush backgroundBrush; backgroundBrush.setColor(backgroundColor); backgroundBrush.setPixmap(backgroundPixmap); setPaper(backgroundBrush); } } void IRCView::setServer(Server* newServer) { m_server = newServer; if (newServer) { TDEAction *action = newServer->getViewContainer()->actionCollection()->action("open_logfile"); Q_ASSERT(action); if(!action) return; m_popup->insertSeparator(); action->plug(m_popup); } } const TQString& IRCView::getContextNick() const { return m_currentNick; } void IRCView::clearContextNick() { m_currentNick = TQString(); } void IRCView::clear() { m_buffer = TQString(); KTextBrowser::setText(""); wipeLineParagraphs(); } void IRCView::highlightedSlot(const TQString& _link) { TQString link = _link; // HACK Replace % with \x03 in the url to keep TQt from doing stupid things link = link.replace ('\x03', "%"); //Hack to handle the fact that we get a decoded url link = KURL::fromPathOrURL(link).url(); // HACK:Use space as a placeholder for \ as TQt tries to be clever and does a replace to / in urls in TQTextEdit if(link.startsWith("#")) { link = link.replace(' ', "\\"); } //we just saw this a second ago. no need to reemit. if (link == m_lastStatusText && !link.isEmpty()) return; // remember current URL to overcome link clicking problems in TQTextBrowser m_highlightedURL = link; if (link.isEmpty()) { if (!m_lastStatusText.isEmpty()) { emit clearStatusBarTempText(); m_lastStatusText = TQString(); } } else { m_lastStatusText = link; } if(!link.startsWith("#")) { m_isOnNick = false; m_isOnChannel = false; if (!link.isEmpty()) { //link therefore != m_lastStatusText so emit with this new text emit setStatusBarTempText(link); } if (link.isEmpty() && m_copyUrlMenu) { m_popup->removeItem(CopyUrl); m_popup->removeItem(Bookmark); m_popup->removeItem(SaveAs); m_popup->setItemVisible(copyUrlMenuSeparator, false); m_copyUrlMenu = false; } else if (!link.isEmpty() && !m_copyUrlMenu) { m_popup->setItemVisible(copyUrlMenuSeparator, true); m_popup->insertItem(SmallIcon("edit-copy"), i18n("Copy URL to Clipboard"), CopyUrl, 1); m_popup->insertItem(SmallIcon("bookmark"), i18n("Add to Bookmarks"), Bookmark, 2); m_popup->insertItem(SmallIcon("document-save-as"), i18n("Save Link As..."), SaveAs, 3); m_copyUrlMenu = true; m_urlToCopy = link; } } else if (link.startsWith("#") && !link.startsWith("##")) { m_currentNick = link.mid(1); m_nickPopup->changeTitle(m_nickPopupId,m_currentNick); m_isOnNick = true; emit setStatusBarTempText(i18n("Open a query with %1").arg(m_currentNick)); } else { // link.startsWith("##") m_currentChannel = link.mid(1); TQString prettyId = m_currentChannel; if (prettyId.length()>15) { prettyId.truncate(15); prettyId.append("..."); } m_channelPopup->changeTitle(m_channelPopupId,prettyId); m_isOnChannel = true; emit setStatusBarTempText(i18n("Join the channel %1").arg(m_currentChannel)); } } void IRCView::openLink(const TQString& url, bool newTab) { if (!url.isEmpty() && !url.startsWith("#")) { if (url.startsWith("irc://")) { KonversationApplication* konvApp = static_cast(kapp); konvApp->getConnectionManager()->connectTo(Konversation::SilentlyReuseConnection, url); } else if (!Preferences::useCustomBrowser() || url.startsWith("mailto:")) { if(newTab && !url.startsWith("mailto:")) { TQCString foundApp, foundObj; TQByteArray data; TQDataStream str(data, IO_WriteOnly); if( TDEApplication::dcopClient()->findObject("konqueror*", "konqueror-mainwindow*", "windowCanBeUsedForTab()", data, foundApp, foundObj, false, 3000)) { DCOPRef ref(foundApp, foundObj); ref.call("newTab", url); } else new KRun(KURL::fromPathOrURL(url)); } else new KRun(KURL::fromPathOrURL(url)); } else { TQString cmd = Preferences::webBrowserCmd(); cmd.replace("%u", url); TDEProcess *proc = new TDEProcess; TQStringList cmdAndArgs = KShell::splitArgs(cmd); *proc << cmdAndArgs; // This code will also work, but starts an extra shell process. // kdDebug() << "IRCView::urlClickSlot(): cmd = " << cmd << endl; // *proc << cmd; // proc->setUseShell(true); proc->start(TDEProcess::DontCare); delete proc; } } //FIXME: Don't do channel links in DCC Chats to begin with since they don't have a server. else if (url.startsWith("##") && m_server && m_server->isConnected()) { TQString channel(url); channel.replace("##", "#"); m_server->sendJoinCommand(channel); } //FIXME: Don't do user links in DCC Chats to begin with since they don't have a server. else if (url.startsWith("#") && m_server && m_server->isConnected()) { TQString recipient(url); recipient.remove("#"); NickInfoPtr nickInfo = m_server->obtainNickInfo(recipient); m_server->addQuery(nickInfo, true /*we initiated*/); } } void IRCView::replaceDecoration(TQString& line, char decoration, char replacement) { int pos; bool decorated = false; while((pos=line.find(decoration))!=-1) { line.replace(pos,1,(decorated) ? TQString("").arg(replacement) : TQString("<%1>").arg(replacement)); decorated = !decorated; } } TQString IRCView::filter(const TQString& line, const TQString& defaultColor, const TQString& whoSent, bool doHighlight, bool parseURL, bool self) { TQString filteredLine(line); KonversationApplication* konvApp = static_cast(kapp); //Since we can't turn off whitespace simplification withouteliminating text wrapping, // if the line starts with a space turn it into a non-breaking space. // (which magically turns back into a space on copy) if (filteredLine[0]==' ') filteredLine[0]='\xA0'; // TODO: Use TQStyleSheet::escape() here // Replace all < with < filteredLine.replace("<","\x0blt;"); // Replace all > with > filteredLine.replace(">", "\x0bgt;"); #if 0 if(!Preferences::disableExpansion()) { TQRegExp boldRe("\\*\\*([a-zA-Z0-9]+)\\*\\*"); TQRegExp underRe("\\_\\_([a-zA-Z0-9]+)\\_\\_"); int position = 0; TQString replacement; while( position >= 0) { position = boldRe.search(filteredLine, position); if( position > -1) { replacement = boldRe.cap(1); replacement = "\x02"+replacement+"\x02"; filteredLine.replace(position,replacement.length()+2,replacement); } position += boldRe.matchedLength(); } position = 0; while( position >= 0) { position = underRe.search(filteredLine, position); if( position > -1) { replacement = underRe.cap(1); replacement = "\x1f"+replacement+"\x1f"; filteredLine.replace(position,replacement.length()+2,replacement); } position += underRe.matchedLength(); } } #endif if(filteredLine.find("\x07") != -1) { if(Preferences::beep()) { kapp->beep(); } } // replace \003 and \017 codes with rich text color codes // captures 1 2 23 4 4 3 1 TQRegExp colorRegExp("(\003([0-9]|0[0-9]|1[0-5]|)(,([0-9]|0[0-9]|1[0-5])|,|)|\017)"); int pos; bool allowColors = Preferences::allowColorCodes(); bool firstColor = true; TQString colorString; while((pos=colorRegExp.search(filteredLine))!=-1) { if(!allowColors) { colorString = TQString(); } else { colorString = (firstColor) ? TQString() : TQString(""); // reset colors on \017 to default value if(colorRegExp.cap(1) == "\017") colorString += ""; else { if(!colorRegExp.cap(2).isEmpty()) { int foregroundColor = colorRegExp.cap(2).toInt(); colorString += ""; } else { colorString += ""; } } firstColor = false; } filteredLine.replace(pos, colorRegExp.cap(0).length(), colorString); } if(!firstColor) filteredLine+=""; // Replace all text decorations // TODO: \017 should reset all textt decorations to plain text replaceDecoration(filteredLine,'\x02','b'); replaceDecoration(filteredLine,'\x09','i'); replaceDecoration(filteredLine,'\x13','s'); replaceDecoration(filteredLine,'\x15','u'); replaceDecoration(filteredLine,'\x16','b'); // should be inverse replaceDecoration(filteredLine,'\x1f','u'); if(parseURL) { filteredLine = Konversation::tagURLs(filteredLine, whoSent); } else { // Change & to & to prevent html entities to do strange things to the text filteredLine.replace('&', "&"); filteredLine.replace("\x0b", "&"); } filteredLine = Konversation::EmotIcon::filter(filteredLine, fontMetrics()); // Highlight TQString ownNick; if (m_server) { ownNick = m_server->getNickname(); } else if (m_chatWin->getType() == ChatWindow::DccChat) { ownNick = static_cast(m_chatWin)->getOwnNick(); } if(doHighlight && (whoSent != ownNick) && !self) { TQString highlightColor; if(Preferences::highlightNick() && filteredLine.lower().find(TQRegExp("(^|[^\\d\\w])" + TQRegExp::escape(ownNick.lower()) + "([^\\d\\w]|$)")) != -1) { // highlight current nickname highlightColor = Preferences::highlightNickColor().name(); m_tabNotification = Konversation::tnfNick; } else { TQPtrList highlightList = Preferences::highlightList(); TQPtrListIterator it(highlightList); Highlight* highlight = it.current(); bool patternFound = false; int index = 0; TQStringList captures; while(highlight) { if(highlight->getRegExp()) { TQRegExp needleReg=highlight->getPattern(); needleReg.setCaseSensitive(false); // highlight regexp in text patternFound = ((filteredLine.find(needleReg) != -1) || // highlight regexp in nickname (whoSent.find(needleReg) != -1)); // remember captured patterns for later captures=needleReg.capturedTexts(); } else { TQString needle=highlight->getPattern(); // highlight patterns in text patternFound = ((filteredLine.find(needle, 0, false) != -1) || // highlight patterns in nickname (whoSent.find(needle, 0, false) != -1)); } if(!patternFound) { ++it; highlight = it.current(); ++index; } else { break; } } if(patternFound) { highlightColor = highlight->getColor().name(); m_highlightColor = highlightColor; m_tabNotification = Konversation::tnfHighlight; if(Preferences::highlightSoundsEnabled() && m_chatWin->notificationsEnabled()) { konvApp->sound()->play(highlight->getSoundURL()); } konvApp->notificationHandler()->highlight(m_chatWin, whoSent, line); m_autoTextToSend = highlight->getAutoText(); // replace %0 - %9 in regex groups for(unsigned int capture=0;capture" + filteredLine + ""; } } else if(doHighlight && (whoSent == ownNick) && Preferences::highlightOwnLines()) { // highlight own lines filteredLine = "" + filteredLine + ""; } // Replace pairs of spaces with " " to preserve some semblance of text wrapping filteredLine.replace(" "," \xA0"); return filteredLine; } TQString IRCView::createNickLine(const TQString& nick, bool encapsulateNick, bool privMsg) { TQString nickLine = "%2"; if(Preferences::useClickableNicks()) { // HACK:Use space as a placeholder for \ as TQt tries to be clever and does a replace to / in urls in TQTextEdit nickLine = "%2"; } if(privMsg) { nickLine.prepend ("-> "); } if(encapsulateNick) nickLine = "<" + nickLine + ">"; if(Preferences::useColoredNicks() && m_server) { TQString nickColor; if (nick != m_server->getNickname()) nickColor = Preferences::nickColor(m_server->obtainNickInfo(nick)->getNickColor()).name(); else nickColor = Preferences::nickColor(8).name(); if(nickColor == "#000000") { nickColor = "#000001"; // HACK Working around TQTextBrowser's auto link coloring } nickLine = ""+nickLine+""; } //FIXME: Another last-minute hack to get DCC Chat colored nicknames // working. We can't use NickInfo::getNickColor() because we don't // have a server. else if (Preferences::useColoredNicks() && m_chatWin->getType() == ChatWindow::DccChat) { TQString ownNick = static_cast(m_chatWin)->getOwnNick(); TQString nickColor; if (nick != ownNick) { int nickvalue = 0; for (uint index = 0; index < nick.length(); index++) { nickvalue += nick[index].unicode(); } nickColor = Preferences::nickColor((nickvalue % 8)).name(); } else nickColor = Preferences::nickColor(8).name(); if(nickColor == "#000000") { nickColor = "#000001"; // HACK Working around TQTextBrowser's auto link coloring } nickLine = ""+nickLine+""; } if(Preferences::useBoldNicks()) nickLine = "" + nickLine + ""; return nickLine; } void IRCView::append(const TQString& nick,const TQString& message) { TQString channelColor = Preferences::color(Preferences::ChannelMessage).name(); if(channelColor == "#000000") { channelColor = "#000001"; // HACK Working around TQTextBrowser's auto link coloring } TQString line; m_tabNotification = Konversation::tnfNormal; TQString nickLine = createNickLine(nick); if(basicDirection(message) == TQChar::DirR) { line = RLE; line += LRE; line += "

" + nickLine + " %1" + PDF + RLM + " %3

\n"; } else { line = "

%1" + nickLine + " %3

\n"; } line = line.arg(timeStamp(), nick, filter(message, channelColor, nick, true)); emit textToLog(TQString("<%1>\t%2").arg(nick).arg(message)); doAppend(line); } void IRCView::insertRememberLine() { m_rememberLineDirtyBit = true; if (!Preferences::automaticRememberLineOnlyOnTextChange()) appendRememberLine(); } void IRCView::cancelRememberLine() { m_rememberLineDirtyBit = false; } void IRCView::appendRememberLine() { m_rememberLineDirtyBit = false; if (m_rememberLineParagraph == paragraphs() - 1) return; if (m_rememberLineParagraph > -1) { removeParagraph(m_rememberLineParagraph); TQValueList newList; TQValueList::ConstIterator it; for (it = m_markerLineParagraphs.begin(); it != m_markerLineParagraphs.end(); ++it) { if ((*it) < m_rememberLineParagraph) newList << (*it); else if ((*it) > m_rememberLineParagraph) newList << (*it) - 1; } m_markerLineParagraphs = newList; } repaintChanged(); appendLine(Preferences::color(Preferences::CommandMessage).name()); m_rememberLineParagraph = paragraphs() - 1; } void IRCView::insertMarkerLine() { qHeapSort(m_markerLineParagraphs); if (m_markerLineParagraphs.last() == paragraphs() - 1) return; bool rememberLineDirtyBit = m_rememberLineDirtyBit; m_rememberLineDirtyBit = false; appendLine(Preferences::color(Preferences::ActionMessage).name()); m_rememberLineDirtyBit = rememberLineDirtyBit; m_markerLineParagraphs.append(paragraphs() - 1); } void IRCView::appendLine(const TQString& color) { TQColor channelColor = Preferences::color(Preferences::ChannelMessage); TQString line = "




\n"; doAppend(line, true); } void IRCView::clearLines() { if (m_rememberLineParagraph > -1) m_markerLineParagraphs.append(m_rememberLineParagraph); if (m_markerLineParagraphs.count() > 0) { qHeapSort(m_markerLineParagraphs); TQValueList::ConstIterator it; int removeCounter = 0; for (it = m_markerLineParagraphs.begin(); it != m_markerLineParagraphs.end(); ++it) { removeParagraph((*it) - removeCounter); removeCounter++; } wipeLineParagraphs(); repaintChanged(); } } bool IRCView::hasLines() { if (m_rememberLineParagraph > -1 || m_markerLineParagraphs.count() > 0) return true; return false; } void IRCView::updateLineParagraphs(int numRemoved) { if (m_rememberLineParagraph - numRemoved < 0) m_rememberLineParagraph = -1; else m_rememberLineParagraph -= numRemoved; if (!m_markerLineParagraphs.isEmpty()) { TQValueList newMarkerLineParagraphs; TQValueList::const_iterator it; for (it = m_markerLineParagraphs.begin(); it != m_markerLineParagraphs.end(); ++it) { if ((*it) - numRemoved >= 0) newMarkerLineParagraphs << ((*it) - numRemoved); } m_markerLineParagraphs = newMarkerLineParagraphs; } } void IRCView::wipeLineParagraphs() { m_markerLineParagraphs.clear(); m_rememberLineParagraph = -1; } void IRCView::appendRaw(const TQString& message, bool suppressTimestamps, bool self) { TQColor channelColor=Preferences::color(Preferences::ChannelMessage); TQString line; m_tabNotification = Konversation::tnfNone; if(suppressTimestamps) { line = TQString("

" + message + "

\n"); } else { line = TQString("

" + timeStamp() + " " + message + "

\n"); } doAppend(line, self); } void IRCView::appendQuery(const TQString& nick, const TQString& message, bool inChannel) { TQString queryColor=Preferences::color(Preferences::QueryMessage).name(); if(queryColor == "#000000") { queryColor = "#000001"; // HACK Working around TQTextBrowser's auto link coloring } TQString line; m_tabNotification = Konversation::tnfPrivate; TQString nickLine = createNickLine(nick, true, inChannel); if(basicDirection(message) == TQChar::DirR) { line = RLE; line += LRE; line += "

" + nickLine + " %1" + PDF + " %3

\n"; } else { line = "

%1 " + nickLine + " %3

\n"; } line = line.arg(timeStamp(), nick, filter(message, queryColor, nick, true)); emit textToLog(TQString("<%1>\t%2").arg(nick).arg(message)); doAppend(line); } void IRCView::appendChannelAction(const TQString& nick,const TQString& message) { m_tabNotification = Konversation::tnfNormal; appendAction(nick, message); } void IRCView::appendQueryAction(const TQString& nick,const TQString& message) { m_tabNotification = Konversation::tnfPrivate; appendAction(nick, message); } void IRCView::appendAction(const TQString& nick,const TQString& message) { TQString actionColor=Preferences::color(Preferences::ActionMessage).name(); // HACK Working around TQTextBrowser's auto link coloring if (actionColor == "#000000") actionColor = "#000001"; TQString line; TQString nickLine = createNickLine(nick, false); if (basicDirection(message) == TQChar::DirR) { line = RLE; line += LRE; line += "

" + nickLine + " * %1" + PDF + " %3

\n"; } else { line = "

%1 * " + nickLine + " %3

\n"; } line = line.arg(timeStamp(), nick, filter(message, actionColor, nick, true)); emit textToLog(TQString("\t * %1 %2").arg(nick).arg(message)); doAppend(line); } void IRCView::appendServerMessage(const TQString& type, const TQString& message, bool parseURL) { TQString serverColor = Preferences::color(Preferences::ServerMessage).name(); m_tabNotification = Konversation::tnfControl; // Fixed width font option for MOTD TQString fixed; if(Preferences::fixedMOTD() && !m_fontDataBase.isFixedPitch(font().family())) { if(type == i18n("MOTD")) fixed=" face=\"" + TDEGlobalSettings::fixedFont().family() + "\""; } TQString line; if(basicDirection(message) == TQChar::DirR) { line = RLE; line += LRE; line += "

[%2] %1" + PDF + " %3

\n"; } else { line = "

%1 [%2] %3

\n"; } if(type != i18n("Notify")) line = line.arg(timeStamp(), type, filter(message, serverColor, 0 , true, parseURL)); else line = ""+line.arg(timeStamp(), type, message)+""; emit textToLog(TQString("%1\t%2").arg(type).arg(message)); doAppend(line); } void IRCView::appendCommandMessage(const TQString& type,const TQString& message, bool important, bool parseURL, bool self) { if (Preferences::hideUnimportantEvents() && !important) return; TQString commandColor = Preferences::color(Preferences::CommandMessage).name(); TQString line; TQString prefix="***"; m_tabNotification = Konversation::tnfControl; if(type == i18n("Join")) { prefix="-->"; parseURL=false; } else if(type == i18n("Part") || type == i18n("Quit")) { prefix="<--"; } prefix=TQStyleSheet::escape(prefix); if(basicDirection(message) == TQChar::DirR) { line = RLE; line += LRE; line += "

%2 %1" + PDF + " %3

\n"; } else { line = "

%1 %2 %3

\n"; } line = line.arg(timeStamp(), prefix, filter(message, commandColor, 0, true, parseURL, self)); emit textToLog(TQString("%1\t%2").arg(type).arg(message)); doAppend(line, self); } void IRCView::appendBacklogMessage(const TQString& firstColumn,const TQString& rawMessage) { TQString time; TQString message = rawMessage; TQString nick = firstColumn; TQString backlogColor = Preferences::color(Preferences::BacklogMessage).name(); m_tabNotification = Konversation::tnfNone; time = nick.section(' ', 0, 4); nick = nick.section(' ', 5); if(!nick.isEmpty() && !nick.startsWith("<") && !nick.startsWith("*")) { nick = '|' + nick + '|'; } // Nicks are in "" format so replace the "<>" nick.replace("<","<"); nick.replace(">",">"); TQString line; if(basicDirection(message) == TQChar::DirR) { line = "

%2 %1 %3

\n"; } else { line = "

%1 %2 %3

\n"; } line = line.arg(time, nick, filter(message, backlogColor, NULL, false, false)); doAppend(line); } //without any display update stuff that freaks out the scrollview void IRCView::removeSelectedText( int selNum ) { TQTextDocument* doc=document(); for ( int i = 0; i < (int)doc->numSelections(); ++i ) { if ( i == selNum ) continue; doc->removeSelection( i ); } // ...snip... doc->removeSelectedText( selNum, TQTextEdit::textCursor() ); // ...snip... } void IRCView::scrollToBottom() { // TQTextEdit::scrollToBottom does sync() too, but we don't want it because its slow setContentsPos( contentsX(), contentsHeight() - visibleHeight() ); } void IRCView::ensureCursorVisible() { if (!m_disableEnsureCursorVisible) TQTextEdit::ensureCursorVisible(); } void IRCView::doAppend(const TQString& newLine, bool self) { if (m_rememberLineDirtyBit) appendRememberLine(); // Add line to buffer TQString line(newLine); if (!self && m_chatWin) m_chatWin->activateTabNotification(m_tabNotification); // scroll view only if the scroll bar is already at the bottom bool doScroll = ( KTextBrowser::verticalScrollBar()->value() == KTextBrowser::verticalScrollBar()->maxValue()); line.remove('\n'); // TODO why have newlines? we get

, so the \n are unnecessary... bool up = KTextBrowser::viewport()->isUpdatesEnabled(); KTextBrowser::viewport()->setUpdatesEnabled(false); int paraFrom, indexFrom, paraTo, indexTo; bool textselected = hasSelectedText(); // Remember the selection so we don't loose it when adding the new line if(textselected) getSelection(¶From, &indexFrom, ¶To, &indexTo); document()->lastParagraph()->format(); KTextBrowser::append(line); document()->lastParagraph()->format(); // get maximum number of lines we want to have in the scollback buffer int sbm = Preferences::scrollbackMax(); // Explanation: the scrolling mechanism cannot handle the buffer changing when the scrollbar is not // at an end, so the scrollbar wets its pants and forgets who it is for ten minutes // Also make sure not to delete any lines if maximum lines of scrollback is set to 0 (unlimited) if (sbm && doScroll) { int numRemoved = paragraphs() - sbm; if (numRemoved > 0) { for (int index = numRemoved; index > 0; --index) { removeParagraph(0); } updateLineParagraphs(numRemoved); if (textselected) { paraFrom -= numRemoved; paraTo -= numRemoved; } } } resizeContents(contentsWidth(), document()->height()); KTextBrowser::viewport()->setUpdatesEnabled(up); // Restore selection if(textselected && paraFrom >= 0 && paraTo >= 0) { // HACK: do not change scrollback position: setSelection() calls // ensureCursorVisible() m_disableEnsureCursorVisible = true; setSelection(paraFrom, indexFrom, paraTo, indexTo); m_disableEnsureCursorVisible = false; } if (doScroll) updateScrollBarPos(); //FIXME: Disable auto-text for DCC Chats since we don't have a server // to parse wildcards. if (!m_autoTextToSend.isEmpty() && m_server) { // replace placeholders in autoText TQString sendText = m_server->parseWildcards(m_autoTextToSend,m_server->getNickname(), TQString(), TQString(), TQString(), TQString()); // avoid recursion due to signalling m_autoTextToSend = TQString(); // send signal only now emit autoText(sendText); } else { m_autoTextToSend = TQString(); } if (!m_lastStatusText.isEmpty()) emit clearStatusBarTempText(); } // remember if scrollbar was positioned at the end of the text or not void IRCView::hideEvent(TQHideEvent* /* event */) { m_resetScrollbar = ((contentsHeight()-visibleHeight()) == contentsY()); } // Workaround to scroll to the end of the TextView when it's shown void IRCView::showEvent(TQShowEvent* event) { Q_UNUSED(event); // did the user scroll the view to the end of the text before hiding? if (m_resetScrollbar) { // NOTE: when the view has not been painted yet, contentsHeight() might // not return accurate information and hence this code won't reset // scrollback properly in such a case. See paintEvent() hack below. moveCursor(MoveEnd,false); ensureVisible(0,contentsHeight()); } } void IRCView::paintEvent(TQPaintEvent* event) { // HACK: if the widget is being painted for the first time, call // showEvent() which will reset scrollback position as needed. It seems // that at the time this event is triggered, TQTextEdit provides more // accurate information than when showEvent() is triggered. if (!m_wasPainted) { m_wasPainted = true; showEvent(0); } KTextBrowser::paintEvent(event); } void IRCView::contentsMouseReleaseEvent(TQMouseEvent *ev) { if (ev->button() == TQt::MidButton) { if(m_copyUrlMenu) { openLink(m_urlToCopy,true); return; } else { emit textPasted(true); return; } } if (ev->button() == TQt::LeftButton) { if (m_mousePressed) { if (ev->state() == (TQt::LeftButton|TQt::ShiftButton)) saveLinkAs(m_highlightedURL); else openLink(m_highlightedURL); m_mousePressed = false; return; } } KTextBrowser::contentsMouseReleaseEvent(ev); } void IRCView::contentsMousePressEvent(TQMouseEvent* ev) { if (ev->button() == TQt::LeftButton) { m_urlToDrag = m_highlightedURL; if (!m_urlToDrag.isNull()) { m_mousePressed = true; m_pressPosition = ev->pos(); return; } } KTextBrowser::contentsMousePressEvent(ev); } void IRCView::contentsMouseMoveEvent(TQMouseEvent* ev) { if (m_mousePressed && (m_pressPosition - ev->pos()).manhattanLength() > TQApplication::startDragDistance()) { m_mousePressed = false; removeSelection(); KURL ux = KURL::fromPathOrURL(m_urlToDrag); if (m_server && m_urlToDrag.startsWith("##")) { //FIXME consistent IRC URL serialization ux = TQString("irc://%1:%2/%3").arg(m_server->getServerName()).arg(m_server->getPort()).arg(m_urlToDrag.mid(2)); } else if (m_urlToDrag.startsWith("#")) { ux = m_urlToDrag.mid(1); } KURLDrag* u = new KURLDrag(ux, viewport()); u->drag(); return; } KTextBrowser::contentsMouseMoveEvent(ev); } void IRCView::contentsContextMenuEvent(TQContextMenuEvent* ev) { bool block = contextMenu(ev); // HACK Replace % with \x03 in the url to keep TQt from doing stupid things m_highlightedURL = anchorAt(viewportToContents(mapFromGlobal(TQCursor::pos()))); m_highlightedURL = m_highlightedURL.replace('\x03', "%"); // Hack to counter the fact that we're given an decoded url m_highlightedURL = KURL::fromPathOrURL(m_highlightedURL).url(); if (m_highlightedURL.isEmpty()) viewport()->setCursor(TQt::ArrowCursor); if(m_highlightedURL.startsWith("#")) { // HACK:Use space as a placeholder for \ as TQt tries to be clever and does a replace to / in urls in TQTextEdit m_highlightedURL = m_highlightedURL.replace(' ', "\\"); } if (!block) KTextBrowser::contentsContextMenuEvent(ev); } bool IRCView::contextMenu(TQContextMenuEvent* ce) { if (m_server && m_isOnNick && m_nickPopup->isEnabled()) { updateNickMenuEntries(m_nickPopup, getContextNick()); if(m_nickPopup->exec(ce->globalPos()) == -1) clearContextNick(); m_isOnNick = false; } else if (m_server && m_isOnChannel && m_channelPopup->isEnabled()) { m_channelPopup->exec(ce->globalPos()); m_isOnChannel = false; } else { TDEActionCollection* actionCollection = KonversationApplication::instance()->getMainWindow()->actionCollection(); TDEToggleAction* toggleMenuBarAction = static_cast(actionCollection->action("options_show_menubar")); if (toggleMenuBarAction && !toggleMenuBarAction->isChecked()) { toggleMenuBarAction->plug(m_popup, 0); m_popup->setItemVisible(toggleMenuBarSeparator, true); } m_popup->setItemEnabled(Copy,(hasSelectedText())); TDEAction* channelSettingsAction = 0; if (m_chatWin->getType() == ChatWindow::Channel) { channelSettingsAction = KonversationApplication::instance()->getMainWindow()->actionCollection()->action("channel_settings"); if (channelSettingsAction) channelSettingsAction->plug(m_popup); } if (m_chatWin->getType() == ChatWindow::Query) { updateNickMenuEntries(m_popup, m_chatWin->getName()); clearContextNick(); } int r = m_popup->exec(ce->globalPos()); switch (r) { case -1: // dummy. -1 means, no entry selected. we don't want -1to go in default, so // we catch it here break; case Copy: copy(); break; case CopyUrl: { TQClipboard *cb = TDEApplication::kApplication()->clipboard(); cb->setText(m_urlToCopy,TQClipboard::Selection); cb->setText(m_urlToCopy,TQClipboard::Clipboard); break; } case SelectAll: selectAll(); break; case Search: search(); break; case SendFile: emit sendFile(); break; case Bookmark: { KBookmarkManager* bm = KBookmarkManager::userBookmarksManager(); KBookmarkGroup bg = bm->addBookmarkDialog(m_urlToCopy, TQString()); bm->save(); bm->emitChanged(bg); break; } case SaveAs: saveLinkAs(m_urlToCopy); break; default: emit extendedPopup(r); } if (toggleMenuBarAction) { toggleMenuBarAction->unplug(m_popup); m_popup->setItemVisible(toggleMenuBarSeparator, false); } if (channelSettingsAction) channelSettingsAction->unplug(m_popup); } return true; } void IRCView::setupNickPopupMenu() { m_nickPopup = new TDEPopupMenu(this,"nicklist_context_menu"); m_modes = new TDEPopupMenu(this,"nicklist_modes_context_submenu"); m_kickban = new TDEPopupMenu(this,"nicklist_kick_ban_context_submenu"); m_nickPopupId= m_nickPopup->insertTitle(m_currentNick); m_nickPopup->insertItem(i18n("&Whois"),Konversation::Whois); m_nickPopup->insertItem(i18n("&Version"),Konversation::Version); m_nickPopup->insertItem(i18n("&Ping"),Konversation::Ping); m_nickPopup->insertSeparator(); m_modes->insertItem(i18n("Give Op"),Konversation::GiveOp); m_modes->insertItem(i18n("Take Op"),Konversation::TakeOp); m_modes->insertItem(i18n("Give Voice"),Konversation::GiveVoice); m_modes->insertItem(i18n("Take Voice"),Konversation::TakeVoice); m_nickPopup->insertItem(i18n("Modes"),m_modes,Konversation::ModesSub); m_kickban->insertItem(i18n("Kick"),Konversation::Kick); m_kickban->insertItem(i18n("Kickban"),Konversation::KickBan); m_kickban->insertItem(i18n("Ban Nickname"),Konversation::BanNick); m_kickban->insertSeparator(); m_kickban->insertItem(i18n("Ban *!*@*.host"),Konversation::BanHost); m_kickban->insertItem(i18n("Ban *!*@domain"),Konversation::BanDomain); m_kickban->insertItem(i18n("Ban *!user@*.host"),Konversation::BanUserHost); m_kickban->insertItem(i18n("Ban *!user@domain"),Konversation::BanUserDomain); m_kickban->insertSeparator(); m_kickban->insertItem(i18n("Kickban *!*@*.host"),Konversation::KickBanHost); m_kickban->insertItem(i18n("Kickban *!*@domain"),Konversation::KickBanDomain); m_kickban->insertItem(i18n("Kickban *!user@*.host"),Konversation::KickBanUserHost); m_kickban->insertItem(i18n("Kickban *!user@domain"),Konversation::KickBanUserDomain); m_nickPopup->insertItem(i18n("Kick / Ban"),m_kickban,Konversation::KickBanSub); m_nickPopup->insertItem(i18n("Ignore"), Konversation::IgnoreNick); m_nickPopup->insertItem(i18n("Unignore"), Konversation::UnignoreNick); m_nickPopup->setItemVisible(Konversation::IgnoreNick, false); m_nickPopup->setItemVisible(Konversation::UnignoreNick, false); m_nickPopup->insertSeparator(); m_nickPopup->insertItem(i18n("Open Query"),Konversation::OpenQuery); if (kapp->authorize("allow_downloading")) { m_nickPopup->insertItem(SmallIcon("2rightarrow"),i18n("Send &File..."),Konversation::DccSend); } m_nickPopup->insertSeparator(); m_nickPopup->insertItem(i18n("Add to Watched Nicks"), Konversation::AddNotify); connect(m_nickPopup, TQ_SIGNAL(activated(int)), this, TQ_SIGNAL(popupCommand(int))); connect(m_modes, TQ_SIGNAL(activated(int)), this, TQ_SIGNAL(popupCommand(int))); connect(m_kickban, TQ_SIGNAL(activated(int)), this, TQ_SIGNAL(popupCommand(int))); } void IRCView::updateNickMenuEntries(TQPopupMenu* popup, const TQString& nickname) { if (popup) { if (Preferences::isIgnored(nickname)) { popup->setItemVisible(Konversation::UnignoreNick, true); popup->setItemVisible(Konversation::IgnoreNick, false); } else { popup->setItemVisible(Konversation::IgnoreNick, true); popup->setItemVisible(Konversation::UnignoreNick, false); } if (!m_server || !m_server->getServerGroup()) popup->setItemEnabled(Konversation::AddNotify, false); else if (!m_server->isConnected()) popup->setItemEnabled(Konversation::AddNotify, false); else if (!Preferences::hasNotifyList(m_server->getServerGroup()->id())) popup->setItemEnabled(Konversation::AddNotify, false); else if (Preferences::isNotify(m_server->getServerGroup()->id(), nickname)) popup->setItemEnabled(Konversation::AddNotify, false); else popup->setItemEnabled(Konversation::AddNotify, true); } } void IRCView::setupQueryPopupMenu() { m_nickPopup = new TDEPopupMenu(this,"query_context_menu"); m_nickPopupId = m_nickPopup->insertTitle(m_currentNick); m_nickPopup->insertItem(i18n("&Whois"),Konversation::Whois); m_nickPopup->insertItem(i18n("&Version"),Konversation::Version); m_nickPopup->insertItem(i18n("&Ping"),Konversation::Ping); m_nickPopup->insertSeparator(); m_nickPopup->insertItem(i18n("Ignore"), Konversation::IgnoreNick); m_nickPopup->insertItem(i18n("Unignore"), Konversation::UnignoreNick); m_nickPopup->setItemVisible(Konversation::IgnoreNick, false); m_nickPopup->setItemVisible(Konversation::UnignoreNick, false); if (kapp->authorize("allow_downloading")) m_nickPopup->insertItem(SmallIcon("2rightarrow"),i18n("Send &File..."),Konversation::DccSend); m_nickPopup->insertItem(i18n("Add to Watched Nicks"), Konversation::AddNotify); connect(m_nickPopup, TQ_SIGNAL(activated(int)), this, TQ_SIGNAL(popupCommand(int))); } void IRCView::setupChannelPopupMenu() { m_channelPopup = new TDEPopupMenu(this,"channel_context_menu"); m_channelPopupId = m_channelPopup->insertTitle(m_currentChannel); m_channelPopup->insertItem(i18n("&Join"),Konversation::Join); m_channelPopup->insertItem(i18n("Get &user list"),Konversation::Names); m_channelPopup->insertItem(i18n("Get &topic"),Konversation::Topic); connect(m_channelPopup, TQ_SIGNAL(activated(int)), this, TQ_SIGNAL(popupCommand(int))); } void IRCView::setNickAndChannelContextMenusEnabled(bool enable) { if (m_nickPopup) m_nickPopup->setEnabled(enable); if (m_channelPopup) m_channelPopup->setEnabled(enable); } void IRCView::search() { emit doSearch(); } void IRCView::searchAgain() { if (m_pattern.isEmpty()) { emit doSearch(); } else { // next search must begin one index before / after the last search // depending on the search direction. if(m_forward) { ++m_findIndex; if(m_findIndex == paragraphLength(m_findParagraph)) { m_findIndex = 0; ++m_findParagraph; } } else { if(m_findIndex) { --m_findIndex; } else { --m_findParagraph; m_findIndex = paragraphLength(m_findParagraph); } } if(!find(m_pattern, m_caseSensitive, m_wholeWords, m_forward, &m_findParagraph, &m_findIndex)) { KMessageBox::information(this,i18n("No matches found for \"%1\".").arg(m_pattern),i18n("Information")); } } } bool IRCView::search(const TQString& pattern, bool caseSensitive, bool wholeWords, bool forward, bool fromCursor) { m_pattern = pattern; m_caseSensitive = caseSensitive; m_wholeWords = wholeWords; m_forward = forward; m_fromCursor = fromCursor; if (m_pattern.isEmpty()) return true; if (!m_fromCursor) { if(m_forward) { m_findParagraph = 1; m_findIndex = 1; } else { m_findParagraph = paragraphs(); m_findIndex = paragraphLength(paragraphs()); } } return searchNext(); } bool IRCView::searchNext(bool reversed) { if (m_pattern.isEmpty()) return true; bool fwd = (reversed ? !m_forward : m_forward); // next search must begin one index before / after the last search // depending on the search direction. if (fwd) { ++m_findIndex; if(m_findIndex == paragraphLength(m_findParagraph)) { m_findIndex = 0; ++m_findParagraph; } } else { if (m_findIndex) { --m_findIndex; } else { --m_findParagraph; m_findIndex = paragraphLength(m_findParagraph); } } return find(m_pattern, m_caseSensitive, m_wholeWords, fwd, &m_findParagraph, &m_findIndex); } // other windows can link own menu entries here TQPopupMenu* IRCView::getPopup() const { return m_popup; } // for more information about these RTFM // http://www.unicode.org/reports/tr9/ // http://www.w3.org/TR/unicode-xml/ TQChar IRCView::LRM = (ushort)0x200e; // Right-to-Left Mark TQChar IRCView::RLM = (ushort)0x200f; // Left-to-Right Mark TQChar IRCView::LRE = (ushort)0x202a; // Left-to-Right Embedding TQChar IRCView::RLE = (ushort)0x202b; // Right-to-Left Embedding TQChar IRCView::RLO = (ushort)0x202e; // Right-to-Left Override TQChar IRCView::LRO = (ushort)0x202d; // Left-to-Right Override TQChar IRCView::PDF = (ushort)0x202c; // Previously Defined Format TQChar::Direction IRCView::basicDirection(const TQString &string) { // The following code decides between LTR or RTL direction for // a line based on the amount of each type of characters pre- // sent. It does so by counting, but stops when one of the two // counters becomes higher than half of the string length to // avoid unnecessary work. unsigned int pos = 0; unsigned int rtl_chars = 0; unsigned int ltr_chars = 0; unsigned int str_len = string.length(); unsigned int str_half_len = str_len/2; for(pos=0; pos < str_len; pos++) { if (!(string[pos].isNumber() || string[pos].isSymbol() || string[pos].isSpace() || string[pos].isPunct() || string[pos].isMark())) { switch(string[pos].direction()) { case TQChar::DirL: case TQChar::DirLRO: case TQChar::DirLRE: ltr_chars++; break; case TQChar::DirR: case TQChar::DirAL: case TQChar::DirRLO: case TQChar::DirRLE: rtl_chars++; break; default: break; } } if (ltr_chars > str_half_len) return TQChar::DirL; else if (rtl_chars > str_half_len) return TQChar::DirR; } if (rtl_chars > ltr_chars) return TQChar::DirR; else return TQChar::DirL; } void IRCView::contentsDragMoveEvent(TQDragMoveEvent *e) { if(acceptDrops() && TQUriDrag::canDecode(e)) e->accept(); } void IRCView::contentsDropEvent(TQDropEvent *e) { TQStrList s; if(TQUriDrag::decode(e,s)) emit filesDropped(s); } TQString IRCView::timeStamp() { if(Preferences::timestamping()) { TQTime time = TQTime::currentTime(); TQString timeColor = Preferences::color(Preferences::Time).name(); TQString timeFormat = Preferences::timestampFormat(); TQString timeString; if(!Preferences::showDate()) { timeString = TQString("[%1] ").arg(time.toString(timeFormat)); } else { TQDate date = TQDate::currentDate(); timeString = TQString("[%1 %2] ") .arg(TDEGlobal::locale()->formatDate(date, true /*short format*/), time.toString(timeFormat)); } return timeString; } return TQString(); } void IRCView::setChatWin(ChatWindow* chatWin) { m_chatWin = chatWin; if(m_chatWin->getType()==ChatWindow::Channel) setupNickPopupMenu(); else setupQueryPopupMenu(); setupChannelPopupMenu(); } void IRCView::keyPressEvent(TQKeyEvent* e) { KKey key(e); if (TDEStdAccel::copy().contains(key)) { copy(); e->accept(); return; } else if (TDEStdAccel::paste().contains(key)) { emit textPasted(false); e->accept(); return; } KTextBrowser::keyPressEvent(e); } void IRCView::resizeEvent(TQResizeEvent* e) { bool doScroll = ( KTextBrowser::verticalScrollBar()->value() == KTextBrowser::verticalScrollBar()->maxValue()); KTextBrowser::resizeEvent(e); if(doScroll) { TQTimer::singleShot(0, this, TQ_SLOT(updateScrollBarPos())); } } void IRCView::updateScrollBarPos() { ensureVisible(contentsX(), contentsHeight()); repaintContents(false); } void IRCView::saveLinkAs(const TQString& url) { KURL source(url); KFileDialog dialog(":SaveLinkAs", TQString (), this, "savelinkdia", true); dialog.setCaption(i18n("Save Link As")); dialog.setSelection(source.fileName()); if(dialog.exec() == TQDialog::Rejected) return; KURL destination = dialog.selectedURL(); TDEIO::copyAs(source, destination); } #include "ircview.moc"