| 1 | // -*- c-basic-offset: 2 -*- |
|---|
| 2 | /* |
|---|
| 3 | * This file is part of Licq, an instant messaging client for UNIX. |
|---|
| 4 | * Copyright (C) 2002-2006 Licq developers |
|---|
| 5 | * |
|---|
| 6 | * Licq is free software; you can redistribute it and/or modify |
|---|
| 7 | * it under the terms of the GNU General Public License as published by |
|---|
| 8 | * the Free Software Foundation; either version 2 of the License, or |
|---|
| 9 | * (at your option) any later version. |
|---|
| 10 | * |
|---|
| 11 | * Licq is distributed in the hope that it will be useful, |
|---|
| 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|---|
| 14 | * GNU General Public License for more details. |
|---|
| 15 | * |
|---|
| 16 | * You should have received a copy of the GNU General Public License |
|---|
| 17 | * along with Licq; if not, write to the Free Software |
|---|
| 18 | * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
|---|
| 19 | */ |
|---|
| 20 | |
|---|
| 21 | // written by Graham Roff <graham@licq.org> |
|---|
| 22 | // contributions by Dirk A. Mueller <dirk@licq.org> |
|---|
| 23 | |
|---|
| 24 | #include "mlview.h" |
|---|
| 25 | |
|---|
| 26 | #include "config.h" |
|---|
| 27 | |
|---|
| 28 | #include <QApplication> |
|---|
| 29 | #include <QClipboard> |
|---|
| 30 | #include <QContextMenuEvent> |
|---|
| 31 | #include <QTextDocumentFragment> |
|---|
| 32 | #include <QMenu> |
|---|
| 33 | #include <QRegExp> |
|---|
| 34 | |
|---|
| 35 | #include "config/emoticons.h" |
|---|
| 36 | #include "config/general.h" |
|---|
| 37 | #include "core/licqgui.h" |
|---|
| 38 | |
|---|
| 39 | using namespace LicqQtGui; |
|---|
| 40 | /* TRANSLATOR LicqQtGui::MLView */ |
|---|
| 41 | |
|---|
| 42 | MLView::MLView(QWidget* parent) |
|---|
| 43 | : QTextBrowser(parent), |
|---|
| 44 | m_handleLinks(true) |
|---|
| 45 | { |
|---|
| 46 | setLineWrapMode(QTextEdit::WidgetWidth); |
|---|
| 47 | setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); |
|---|
| 48 | |
|---|
| 49 | updateFont(); |
|---|
| 50 | connect(Config::General::instance(), SIGNAL(fontChanged()), SLOT(updateFont())); |
|---|
| 51 | } |
|---|
| 52 | |
|---|
| 53 | void MLView::appendNoNewLine(const QString& s) |
|---|
| 54 | { |
|---|
| 55 | QTextCursor tc = textCursor(); |
|---|
| 56 | tc.movePosition(QTextCursor::End); |
|---|
| 57 | tc.insertHtml(s); |
|---|
| 58 | } |
|---|
| 59 | |
|---|
| 60 | QString MLView::toRichText(const QString& s, bool highlightURLs, bool useHTML, QRegExp highlight) |
|---|
| 61 | { |
|---|
| 62 | // Expressions to match URIs and Mail addresses |
|---|
| 63 | // If no matching should be done, they will be left empty |
|---|
| 64 | QRegExp reURL; |
|---|
| 65 | QRegExp reMail; |
|---|
| 66 | |
|---|
| 67 | // We must hightlight URLs at this step, before we convert |
|---|
| 68 | // linebreaks to richtext tags and such. Also, check to make sure |
|---|
| 69 | // that the text is not prepared to be highlighted already (by AIM). |
|---|
| 70 | QRegExp reAHREF("<a href", Qt::CaseInsensitive); |
|---|
| 71 | if (highlightURLs && s.indexOf(reAHREF) == -1) |
|---|
| 72 | { |
|---|
| 73 | reURL.setPattern( |
|---|
| 74 | "(?:(https?|ftp)://(.+(:.+)?@)?|www\\d?\\.)" // protocoll://[user[:password]@] or www[digit]. |
|---|
| 75 | "[a-z0-9.-]+\\.([a-z]+|[0-9]+)" // hostname.tld or ip address |
|---|
| 76 | "(:[0-9]+)?" // optional port |
|---|
| 77 | "(/(([-\\w%{}|\\\\^~`;/?:@=&$_.+!*'(),#]|\\[|\\])*[^.,:;?!\\s])*)?"); |
|---|
| 78 | reURL.setMinimal(false); |
|---|
| 79 | reURL.setCaseSensitivity(Qt::CaseInsensitive); |
|---|
| 80 | |
|---|
| 81 | reMail.setPattern( |
|---|
| 82 | "(mailto:)?" |
|---|
| 83 | "[a-z9-0._%+-]+" |
|---|
| 84 | "@" |
|---|
| 85 | "[a-z0-9.-]+\\.(?:[a-z]+|[0-9]+)"); |
|---|
| 86 | reMail.setMinimal(false); |
|---|
| 87 | reMail.setCaseSensitivity(Qt::CaseInsensitive); |
|---|
| 88 | } |
|---|
| 89 | |
|---|
| 90 | // The following will parse through the string adding <a> tags to URIs and |
|---|
| 91 | // Mail addresses while highlighting anything matching the highlight regexp. |
|---|
| 92 | // If a highlight crosses any <a> or </a> the <span> is closed and reopened. |
|---|
| 93 | // If URI and mail matches overlap, only the first one will be tagged. |
|---|
| 94 | |
|---|
| 95 | // Variables to keep track of positions in string while parsing |
|---|
| 96 | // *Pos and *Len is position and length of next match. *End is end of current |
|---|
| 97 | // match. *Pos=-2 is used to mark that next match should be found. *End is >0 |
|---|
| 98 | // when a tag is currently open |
|---|
| 99 | int urlPos = -2; |
|---|
| 100 | int urlLen = 0; |
|---|
| 101 | int urlEnd = 0; |
|---|
| 102 | int mailPos = -2; |
|---|
| 103 | int mailLen = 0; |
|---|
| 104 | int mailEnd = 0; |
|---|
| 105 | int hlPos = -2; |
|---|
| 106 | int hlLen = 0; |
|---|
| 107 | int hlEnd = 0; |
|---|
| 108 | |
|---|
| 109 | // New string build while parsing |
|---|
| 110 | QString text; |
|---|
| 111 | // Anything before lastpos has already been copied from input string (s) to text. |
|---|
| 112 | int lastpos = 0; |
|---|
| 113 | |
|---|
| 114 | const QString highlightStart = "<span style=\"background-color: yellow; color: black\">"; |
|---|
| 115 | const QString highlightEnd = "</span>"; |
|---|
| 116 | |
|---|
| 117 | do |
|---|
| 118 | { |
|---|
| 119 | // Find next match of a regexp but only if it has a pattern defined |
|---|
| 120 | if (hlPos == -2 && !highlight.isEmpty()) |
|---|
| 121 | { |
|---|
| 122 | hlPos = s.indexOf(highlight, qMax(hlEnd, lastpos)); |
|---|
| 123 | hlLen = highlight.matchedLength(); |
|---|
| 124 | } |
|---|
| 125 | if (urlPos == -2 && !reURL.isEmpty()) |
|---|
| 126 | { |
|---|
| 127 | urlPos = s.indexOf(reURL, qMax(urlEnd, lastpos)); |
|---|
| 128 | urlLen = reURL.matchedLength(); |
|---|
| 129 | } |
|---|
| 130 | if (mailPos == -2 && !reMail.isEmpty()) |
|---|
| 131 | { |
|---|
| 132 | mailPos = s.indexOf(reMail, qMax(mailEnd, lastpos)); |
|---|
| 133 | mailLen = reMail.matchedLength(); |
|---|
| 134 | } |
|---|
| 135 | |
|---|
| 136 | // Next value for lastpos. Data between newpos and lastpos can be copied as is |
|---|
| 137 | int newpos; |
|---|
| 138 | // Tags to add after next block of text has been copied |
|---|
| 139 | QString tags; |
|---|
| 140 | // Does a highlight need to be closed and reopened to allow other tag to open/close |
|---|
| 141 | bool breakhl = false; |
|---|
| 142 | |
|---|
| 143 | if (hlEnd > 0 && |
|---|
| 144 | (urlPos < 0 || hlEnd <= urlPos) && (urlEnd == 0 || hlEnd <= urlEnd) && |
|---|
| 145 | (mailPos < 0 || hlEnd <= mailPos) && (mailEnd == 0 || hlEnd <= mailEnd)) |
|---|
| 146 | { |
|---|
| 147 | // End of highlight |
|---|
| 148 | tags = highlightEnd; |
|---|
| 149 | newpos = hlEnd; |
|---|
| 150 | hlEnd = 0; |
|---|
| 151 | } |
|---|
| 152 | else if (hlEnd == 0 && hlPos > -1 && |
|---|
| 153 | (urlPos < 0 || hlPos < urlPos) && (urlEnd == 0 || hlPos < urlEnd) && |
|---|
| 154 | (mailPos < 0 || hlPos < mailPos) && (mailEnd == 0 || hlPos < mailEnd)) |
|---|
| 155 | { |
|---|
| 156 | // Start of highlight |
|---|
| 157 | tags = highlightStart; |
|---|
| 158 | newpos = hlPos; |
|---|
| 159 | hlEnd = hlPos + hlLen; |
|---|
| 160 | hlPos = -2; // Trigger search to continue |
|---|
| 161 | } |
|---|
| 162 | else if (urlEnd > 0) |
|---|
| 163 | { |
|---|
| 164 | // End of URI |
|---|
| 165 | tags = "</a>"; |
|---|
| 166 | breakhl = true; |
|---|
| 167 | newpos = urlEnd; |
|---|
| 168 | urlEnd = 0; |
|---|
| 169 | mailPos = -2; // Make sure we don't have overlapping URL and mail |
|---|
| 170 | } |
|---|
| 171 | else if (mailEnd > 0) |
|---|
| 172 | { |
|---|
| 173 | // End of mail |
|---|
| 174 | tags = "</a>"; |
|---|
| 175 | breakhl = true; |
|---|
| 176 | newpos = mailEnd; |
|---|
| 177 | mailEnd = 0; |
|---|
| 178 | urlPos = -2; // Make sure we don't have overlapping URL and mail |
|---|
| 179 | } |
|---|
| 180 | else if (urlPos > -1 && |
|---|
| 181 | (mailPos == -1 || urlPos <= mailPos)) |
|---|
| 182 | { |
|---|
| 183 | // Start of URI |
|---|
| 184 | QString url = reURL.cap(); |
|---|
| 185 | QString fullurl = (reURL.cap(1).isEmpty() ? QString("http://%1").arg(url) : url); |
|---|
| 186 | tags = "<a href=\"" + fullurl + "\">"; |
|---|
| 187 | breakhl = true; |
|---|
| 188 | newpos = urlPos; |
|---|
| 189 | urlEnd = urlPos + urlLen; |
|---|
| 190 | urlPos = -2; |
|---|
| 191 | } |
|---|
| 192 | else if (mailPos > -1) |
|---|
| 193 | { |
|---|
| 194 | // Start of mail |
|---|
| 195 | QString mail = reMail.cap(); |
|---|
| 196 | QString fullmail = (reMail.cap(1).isEmpty() ? QString("mailto:%1").arg(mail) : mail); |
|---|
| 197 | tags = "<a href=\"" + fullmail + "\">"; |
|---|
| 198 | breakhl = true; |
|---|
| 199 | newpos = mailPos; |
|---|
| 200 | mailEnd = mailPos + mailLen; |
|---|
| 201 | mailPos = -2; |
|---|
| 202 | } |
|---|
| 203 | else |
|---|
| 204 | { |
|---|
| 205 | // Nothing more to do, just get the remainder of the string |
|---|
| 206 | newpos = s.length(); |
|---|
| 207 | } |
|---|
| 208 | |
|---|
| 209 | // Get next block of text that can be copied from input |
|---|
| 210 | QString rawtext = s.mid(lastpos, newpos - lastpos); |
|---|
| 211 | text.append(useHTML ? rawtext : Qt::escape(rawtext)); |
|---|
| 212 | |
|---|
| 213 | // Add tags applicable for this position in the string |
|---|
| 214 | if (breakhl && hlEnd > 0) |
|---|
| 215 | tags = highlightEnd + tags + highlightStart; |
|---|
| 216 | text.append(tags); |
|---|
| 217 | |
|---|
| 218 | lastpos = newpos; |
|---|
| 219 | } |
|---|
| 220 | while (urlEnd > 0 || mailEnd > 0 || hlEnd > 0 || lastpos < s.length()); |
|---|
| 221 | |
|---|
| 222 | Emoticons::self()->parseMessage(text, Emoticons::NormalMode); |
|---|
| 223 | |
|---|
| 224 | // convert linebreaks to <br> |
|---|
| 225 | text.replace(QRegExp("\n"), "<br>\n"); |
|---|
| 226 | // We keep the first space character as-is (to allow line wrapping) |
|---|
| 227 | // and convert the next characters to s (to preserve multiple |
|---|
| 228 | // spaces). |
|---|
| 229 | QRegExp longSpaces(" ([ ]+)"); |
|---|
| 230 | QString cap; |
|---|
| 231 | int pos = 0; |
|---|
| 232 | while ((pos = longSpaces.indexIn(text)) > -1) |
|---|
| 233 | { |
|---|
| 234 | cap = longSpaces.cap(1); |
|---|
| 235 | cap.replace(QRegExp(" "), " "); |
|---|
| 236 | text.replace(pos+1, longSpaces.matchedLength()-1, cap); |
|---|
| 237 | } |
|---|
| 238 | text.replace(QRegExp("\t"), " "); |
|---|
| 239 | |
|---|
| 240 | return text; |
|---|
| 241 | } |
|---|
| 242 | |
|---|
| 243 | void MLView::GotoHome() |
|---|
| 244 | { |
|---|
| 245 | QTextCursor tc = textCursor(); |
|---|
| 246 | tc.movePosition(QTextCursor::Start); |
|---|
| 247 | setTextCursor(tc); |
|---|
| 248 | } |
|---|
| 249 | |
|---|
| 250 | void MLView::GotoEnd() |
|---|
| 251 | { |
|---|
| 252 | QTextCursor tc = textCursor(); |
|---|
| 253 | tc.movePosition(QTextCursor::End); |
|---|
| 254 | setTextCursor(tc); |
|---|
| 255 | } |
|---|
| 256 | |
|---|
| 257 | void MLView::setBackground(const QColor& c) |
|---|
| 258 | { |
|---|
| 259 | QPalette pal = palette(); |
|---|
| 260 | |
|---|
| 261 | pal.setColor(QPalette::Active, QPalette::Base, c); |
|---|
| 262 | pal.setColor(QPalette::Inactive, QPalette::Base, c); |
|---|
| 263 | |
|---|
| 264 | setPalette(pal); |
|---|
| 265 | } |
|---|
| 266 | |
|---|
| 267 | /** @brief Adds "Copy URL" to the popup menu if the user right clicks on a URL. */ |
|---|
| 268 | void MLView::contextMenuEvent(QContextMenuEvent* event) |
|---|
| 269 | { |
|---|
| 270 | QMenu* menu = createStandardContextMenu(); |
|---|
| 271 | |
|---|
| 272 | m_url = anchorAt(event->pos()); |
|---|
| 273 | if (!m_url.isNull() && !m_url.isEmpty()) |
|---|
| 274 | menu->addAction(tr("Copy URL"), this, SLOT(slotCopyUrl())); |
|---|
| 275 | if (hasMarkedText()) |
|---|
| 276 | menu->addAction(tr("Quote"), this, SLOT(makeQuote())); |
|---|
| 277 | |
|---|
| 278 | menu->exec(event->globalPos()); |
|---|
| 279 | delete menu; |
|---|
| 280 | } |
|---|
| 281 | |
|---|
| 282 | /** @brief Adds the contents of m_url to the clipboard. */ |
|---|
| 283 | void MLView::slotCopyUrl() |
|---|
| 284 | { |
|---|
| 285 | if (!m_url.isNull() && !m_url.isEmpty()) |
|---|
| 286 | { |
|---|
| 287 | // This copies m_url to both the normal clipboard (Ctrl+C/V/X) |
|---|
| 288 | // and the selection clipboard (paste with middle mouse button). |
|---|
| 289 | QClipboard *cb = QApplication::clipboard(); |
|---|
| 290 | cb->setText(m_url, QClipboard::Clipboard); |
|---|
| 291 | if (cb->supportsSelection()) |
|---|
| 292 | cb->setText(m_url, QClipboard::Selection); |
|---|
| 293 | } |
|---|
| 294 | } |
|---|
| 295 | |
|---|
| 296 | QMimeData* MLView::createMimeDataFromSelection() const |
|---|
| 297 | { |
|---|
| 298 | QMimeData* result = QTextEdit::createMimeDataFromSelection(); |
|---|
| 299 | |
|---|
| 300 | if (result->hasHtml()) |
|---|
| 301 | { |
|---|
| 302 | QString html = result->html(); |
|---|
| 303 | Emoticons::unparseMessage(html); |
|---|
| 304 | QTextDocumentFragment fragment = |
|---|
| 305 | QTextDocumentFragment::fromHtml(html, document()); |
|---|
| 306 | result->setText(fragment.toPlainText()); |
|---|
| 307 | } |
|---|
| 308 | |
|---|
| 309 | return result; |
|---|
| 310 | } |
|---|
| 311 | |
|---|
| 312 | void MLView::makeQuote() |
|---|
| 313 | { |
|---|
| 314 | QTextCursor cr = textCursor(); |
|---|
| 315 | if (!cr.hasSelection()) |
|---|
| 316 | return; |
|---|
| 317 | |
|---|
| 318 | QString html = cr.selection().toHtml(); |
|---|
| 319 | |
|---|
| 320 | Emoticons::unparseMessage(html); |
|---|
| 321 | |
|---|
| 322 | QString text = QTextDocumentFragment::fromHtml(html).toPlainText(); |
|---|
| 323 | |
|---|
| 324 | text.insert(0, "> "); |
|---|
| 325 | text.replace("\n", "\n> "); |
|---|
| 326 | |
|---|
| 327 | emit quote(text); |
|---|
| 328 | } |
|---|
| 329 | |
|---|
| 330 | void MLView::setForeground(const QColor& c) |
|---|
| 331 | { |
|---|
| 332 | QPalette pal; |
|---|
| 333 | pal.setColor(QPalette::WindowText, c); |
|---|
| 334 | setPalette(pal); |
|---|
| 335 | } |
|---|
| 336 | |
|---|
| 337 | void MLView::setHandleLinks(bool enable) |
|---|
| 338 | { |
|---|
| 339 | m_handleLinks = enable; |
|---|
| 340 | } |
|---|
| 341 | |
|---|
| 342 | void MLView::setSource(const QUrl& url) |
|---|
| 343 | { |
|---|
| 344 | if (m_handleLinks && !url.scheme().isEmpty()) |
|---|
| 345 | LicqGui::instance()->viewUrl(url.toString()); |
|---|
| 346 | } |
|---|
| 347 | |
|---|
| 348 | bool MLView::hasMarkedText() const |
|---|
| 349 | { |
|---|
| 350 | return textCursor().hasSelection(); |
|---|
| 351 | } |
|---|
| 352 | |
|---|
| 353 | QString MLView::markedText() const |
|---|
| 354 | { |
|---|
| 355 | return textCursor().selectedText(); |
|---|
| 356 | } |
|---|
| 357 | |
|---|
| 358 | void MLView::updateFont() |
|---|
| 359 | { |
|---|
| 360 | setFont(Config::General::instance()->historyFont()); |
|---|
| 361 | |
|---|
| 362 | // Get height of current font |
|---|
| 363 | myFontHeight = fontMetrics().height(); |
|---|
| 364 | |
|---|
| 365 | // Set minimum height of text area to one line of text. |
|---|
| 366 | setMinimumHeight(heightForLines(1)); |
|---|
| 367 | } |
|---|
| 368 | |
|---|
| 369 | int MLView::heightForLines(int lines) const |
|---|
| 370 | { |
|---|
| 371 | // We need to add frame width and the added height of the scroll area as |
|---|
| 372 | // we're calculating height for the widget, not the viewport. |
|---|
| 373 | return lines*myFontHeight + height() - viewport()->height() + 2 * frameWidth(); |
|---|
| 374 | } |
|---|
| 375 | |
|---|
| 376 | void MLView::setSizeHintLines(int lines) |
|---|
| 377 | { |
|---|
| 378 | myLinesHint = lines; |
|---|
| 379 | } |
|---|
| 380 | |
|---|
| 381 | QSize MLView::sizeHint() const |
|---|
| 382 | { |
|---|
| 383 | QSize s = QTextBrowser::sizeHint(); |
|---|
| 384 | if (myLinesHint > 0) |
|---|
| 385 | s.setHeight(heightForLines(myLinesHint)); |
|---|
| 386 | return s; |
|---|
| 387 | } |
|---|