1 /***************************************************************************
2  *   Copyright (C) 2010 by David Edmundson <kde@davidedmundson.co.uk>      *
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  *   This program is distributed in the hope that it will be useful,       *
10  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
11  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
12  *   GNU General Public License for more details.                          *
13  *                                                                         *
14  *   You should have received a copy of the GNU General Public License     *
15  *   along with this program; if not, write to the                         *
16  *   Free Software Foundation, Inc.,                                       *
17  *   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA            *
18  ***************************************************************************/
19 
20 #include "adium-theme-view.h"
21 
22 #include "adium-theme-header-info.h"
23 #include "adium-theme-content-info.h"
24 #include "adium-theme-message-info.h"
25 #include "adium-theme-status-info.h"
26 #include "chat-window-style-manager.h"
27 #include "chat-window-style.h"
28 #include "ktp-debug.h"
29 
30 #include <KTp/message-processor.h>
31 
32 #include <QtCore/QFile>
33 #include <QtCore/QTextCodec>
34 #include <QContextMenuEvent>
35 #include <QFontDatabase>
36 #include <QMenu>
37 #include <QDesktopWidget>
38 #include <QWebEngineContextMenuData>
39 #include <QWebEngineProfile>
40 #include <QWebEngineSettings>
41 #include <QApplication>
42 #include <QAction>
43 #include <QLocale>
44 #include <QDesktopServices>
45 
46 #include <KEmoticonsTheme>
47 #include <KSharedConfig>
48 #include <KConfig>
49 #include <KConfigGroup>
50 #include <KMessageBox>
51 #include <KIconLoader>
52 #include <KProtocolInfo>
53 #include <KLocalizedString>
54 
AdiumThemePage(QObject * parent)55 AdiumThemePage::AdiumThemePage(QObject *parent)
56         : QWebEnginePage(parent)
57 {
58 }
59 
acceptNavigationRequest(const QUrl & url,QWebEnginePage::NavigationType navigationType,bool isMainFrame)60 bool AdiumThemePage::acceptNavigationRequest(const QUrl &url, QWebEnginePage::NavigationType navigationType, bool isMainFrame)
61 {
62     if (!isMainFrame && navigationType == QWebEnginePage::NavigationTypeLinkClicked) {
63         /* This might be an iframe (e. g. for the YouTube plugin) */
64         return true;
65     }
66 
67     if (url.fragment() == QLatin1String("x-nextConversation")) {
68         Q_EMIT nextConversation();
69     } else if (url.fragment() == QLatin1String("x-prevConversation")) {
70         Q_EMIT prevConversation();
71     } else if (url.scheme() == QLatin1String("data")) {
72         return true;
73     } else {
74         QDesktopServices::openUrl(url);
75     }
76 
77     // don't let QWebEngineView handle the links, we do
78     return false;
79 }
80 
AdiumThemeView(QWidget * parent)81 AdiumThemeView::AdiumThemeView(QWidget *parent)
82         : QWebEngineView(parent),
83         // check iconPath docs for minus sign in -KIconLoader::SizeLarge
84         m_defaultAvatar(KIconLoader::global()->iconPath(QLatin1String("im-user"),-KIconLoader::SizeLarge)),
85         m_displayHeader(true)
86 {
87     AdiumThemePage *adiumPage = new AdiumThemePage(this);
88     setPage(adiumPage);
89 
90     //blocks QWebEngineView functionality which allows you to change page by dragging a URL onto it.
91     setAcceptDrops(false);
92 
93     setFocusPolicy(Qt::NoFocus);
94 
95     KConfigGroup config(KSharedConfig::openConfig(), "KTpStyleDebug");
96     bool disableCache = config.readEntry("disableStyleCache", false);
97     if (disableCache) {
98         page()->profile()->setHttpCacheType(QWebEngineProfile::NoCache);
99     }
100 
101     connect(page(), &AdiumThemePage::loadFinished, this, &AdiumThemeView::viewLoadFinished);
102 }
103 
load(ChatType chatType)104 void AdiumThemeView::load(ChatType chatType) {
105 
106     //determine the chat window style to use
107     KSharedConfigPtr config = KSharedConfig::openConfig(QLatin1String("ktelepathyrc"));
108     KConfigGroup appearanceConfig;
109 
110     if (chatType == AdiumThemeView::SingleUserChat) {
111         appearanceConfig = config->group("Appearance");
112         m_chatStyle = ChatWindowStyleManager::self()->getValidStyleFromPool(appearanceConfig.readEntry(QLatin1String("styleName"), "renkoo.AdiumMessageStyle"));
113     } else {
114         appearanceConfig = config->group("GroupAppearance");
115         m_chatStyle = ChatWindowStyleManager::self()->getValidStyleFromPool(appearanceConfig.readEntry(QLatin1String("styleName"), "WoshiChat.AdiumMessageStyle"));
116     }
117 
118     if (m_chatStyle == 0 || !m_chatStyle->isValid()) {
119         KMessageBox::error(this, i18n("Failed to load a valid theme. Your installation is broken. Check your kde path. "
120                                       "Will now crash."));
121     }
122 
123     QString variant = appearanceConfig.readEntry(QLatin1String("styleVariant"));
124     if (!variant.isEmpty()) {
125         m_variantName = variant;
126         m_variantPath = m_chatStyle->getVariants().value(variant);
127 
128         // keep m_variantPath, m_variantName empty if there is no variant
129     } else if (!m_chatStyle->getVariants().isEmpty()) {
130         if (m_chatStyle->getVariants().contains(m_chatStyle->defaultVariantName())) {
131             m_variantPath = QString(QLatin1String("Variants/%1.css")).arg(m_chatStyle->defaultVariantName());
132             m_variantName = m_chatStyle->defaultVariantName();
133         } else {
134             m_variantPath = QString(QLatin1String("Variants/%1.css")).arg(m_chatStyle->getVariants().keys().first());
135             m_variantName = m_chatStyle->getVariants().keys().first();
136         }
137     }
138 
139     m_displayHeader = appearanceConfig.readEntry("displayHeader", true);
140 
141     m_useCustomFont = appearanceConfig.readEntry("useCustomFont", false);
142     m_fontFamily = appearanceConfig.readEntry("fontFamily", QWebEngineSettings::globalSettings()->fontFamily(QWebEngineSettings::StandardFont));
143     m_fontSize = appearanceConfig.readEntry("fontSize", QWebEngineSettings::globalSettings()->fontSize(QWebEngineSettings::DefaultFontSize));
144 
145     m_showPresenceChanges = appearanceConfig.readEntry("showPresenceChanges", true);
146     m_showJoinLeaveChanges = appearanceConfig.readEntry("showJoinLeaveChanges", true);
147 }
148 
viewLoadFinished(bool ok)149 void AdiumThemeView::viewLoadFinished(bool ok)
150 {
151     if (ok) {
152         viewReady();
153     }
154 }
155 
contextMenuEvent(QContextMenuEvent * event)156 void AdiumThemeView::contextMenuEvent(QContextMenuEvent *event)
157 {
158     QMenu *menu = new QMenu(this);
159     if (page()->contextMenuData().linkUrl().isValid()) {
160         menu->addAction(page()->action(QWebEnginePage::OpenLinkInThisWindow));
161         menu->addAction(page()->action(QWebEnginePage::CopyLinkToClipboard));
162     }
163     if (!page()->contextMenuData().selectedText().isEmpty()) {
164         menu->addAction(page()->action(QWebEnginePage::Copy));
165     }
166     connect(menu, &QMenu::aboutToHide, menu, &QObject::deleteLater);
167     menu->popup(event->globalPos());
168 }
169 
wheelEvent(QWheelEvent * event)170 void AdiumThemeView::wheelEvent(QWheelEvent* event)
171 {
172     // Zoom text on Ctrl + Scroll
173     if (event->modifiers() & Qt::CTRL) {
174         qreal factor = zoomFactor();
175         if (event->delta() > 0) {
176             factor += 0.1;
177         } else if (event->delta() < 0) {
178             factor -= 0.1;
179         }
180         setZoomFactor(factor);
181         Q_EMIT zoomFactorChanged(factor);
182 
183         event->accept();
184         return;
185     }
186 
187     QWebEngineView::wheelEvent(event);
188 }
189 
mouseReleaseEvent(QMouseEvent * event)190 void AdiumThemeView::mouseReleaseEvent(QMouseEvent *event)
191 {
192     if (event->modifiers() == Qt::NoModifier && event->button() == Qt::MidButton) {
193         Q_EMIT textPasted();
194         event->accept();
195         return;
196     }
197     QWebEngineView::mouseReleaseEvent(event);
198 }
199 
initialise(const AdiumThemeHeaderInfo & chatInfo)200 void AdiumThemeView::initialise(const AdiumThemeHeaderInfo &chatInfo)
201 {
202     QString headerHtml;
203     QString templateHtml = m_chatStyle->getTemplateHtml();
204     QString footerHtml = replaceHeaderKeywords(m_chatStyle->getFooterHtml(), chatInfo);
205     QString extraStyleHtml = QLatin1String("@import url( \"main.css\" );");
206     m_lastContent = AdiumThemeContentInfo();
207 
208     if (templateHtml.isEmpty()) {
209         // if templateHtml is empty, we failed to load the fallback template file
210         KMessageBox::error(this, i18n("Missing required file Template.html - check your installation."));
211     }
212 
213     if (m_displayHeader) {
214         if (chatInfo.isGroupChat()) {
215             // In group chats header should be replaced by topic
216             headerHtml = replaceHeaderKeywords(m_chatStyle->getTopicHtml(), chatInfo);
217         } else {
218             headerHtml = replaceHeaderKeywords(m_chatStyle->getHeaderHtml(), chatInfo);
219         }
220     } //otherwise leave as blank.
221 
222     // set fontFamily and fontSize
223     if (m_useCustomFont) {
224         // use user specified fontFamily and Size
225         settings()->setFontFamily(QWebEngineSettings::StandardFont, m_fontFamily);
226         // We get desktop's DPI and divide it 96, which is the DPI that WebKit has hardcoded in
227         // Then we can just scale the fonts using the obtained coefficient and they should look
228         // good/better on high-dpi screens
229         settings()->setFontSize(QWebEngineSettings::DefaultFontSize, m_fontSize * (QApplication::desktop()->logicalDpiY() / 96.0 ));
230 
231         // since some themes are pretty odd and hardcode fonts to the css we need to override that
232         // with some extra css. this may not work for all themes!
233         extraStyleHtml.append (
234             QString(QLatin1String("\n* {font-family:\"%1\" !important;font-size:%2pt !important};"))
235             .arg( m_fontFamily )
236             .arg( m_fontSize * (QApplication::desktop()->logicalDpiY() / 96.0 ))
237         );
238     } else {
239         // FIXME: we should inform the user if the chatStyle want's to use a fontFamily which is not present on the system
240         QFontDatabase fontDB = QFontDatabase();
241         qCDebug(KTP_TEXTUI_LIB) << "Theme font installed: " << m_chatStyle->defaultFontFamily()
242         << fontDB.families().contains(m_chatStyle->defaultFontFamily());
243 
244         // use theme fontFamily/Size, if not existent, it falls back to systems default font
245         settings()->setFontFamily(QWebEngineSettings::StandardFont, m_chatStyle->defaultFontFamily());
246         // Computing the font size can result in floats and have some rounding errors, so add 0.5 and floor
247         settings()->setFontSize(QWebEngineSettings::DefaultFontSize, qFloor(0.5 + m_chatStyle->defaultFontSize() * (QApplication::desktop()->logicalDpiY() / 96.0 )));
248     }
249 
250     //The templateHtml is in a horrific NSString format.
251     //Want to use this rather than roll our own, as that way we can get templates from themes too
252     //"%@" is each argument.
253     // all other %'s are escaped.
254 
255     // first is baseref
256     // second is extra style code (This is sometimes missing !!!!)
257     // third is variant CSS
258     // 4th is header
259     // 5th is footer
260 
261     templateHtml.replace(QLatin1String("%%"), QLatin1String("%"));
262 
263     int numberOfPlaceholders = templateHtml.count(QLatin1String("%@"));
264 
265     int index = 0;
266     index = templateHtml.indexOf(QLatin1String("%@"), index);
267     templateHtml.replace(index, 2, QString(QLatin1String("file://")).append(m_chatStyle->getStyleBaseHref()));
268 
269     if (numberOfPlaceholders == 5) {
270         index = templateHtml.indexOf(QLatin1String("%@"), index);
271         templateHtml.replace(index, 2, extraStyleHtml);
272     }
273 
274     index = templateHtml.indexOf(QLatin1String("%@"), index);
275     templateHtml.replace(index, 2, m_variantPath);
276 
277     index = templateHtml.indexOf(QLatin1String("%@"), index);
278     templateHtml.replace(index, 2, headerHtml);
279 
280     index = templateHtml.indexOf(QLatin1String("%@"), index);
281     templateHtml.replace(index, 2, footerHtml);
282 
283     // Inject the scripts and the css just before the end of the head tag
284     index = templateHtml.indexOf(QLatin1String("</head>"));
285     templateHtml.insert(index, KTp::MessageProcessor::instance()->header());
286 
287     //qCDebug(KTP_TEXTUI_LIB) << templateHtml;
288 
289     setHtml(templateHtml, QUrl::fromLocalFile(m_chatStyle->getStyleBaseHref()));
290 
291     m_service = chatInfo.service();
292     m_serviceIconPath = chatInfo.serviceIconPath();
293 }
294 
setVariant(const QString & variant)295 void AdiumThemeView::setVariant(const QString &variant)
296 {
297     m_variantName = variant;
298     m_variantPath = QString(QLatin1String("Variants/%1.css")).arg(variant);
299 
300 }
301 
chatStyle() const302 ChatWindowStyle *AdiumThemeView::chatStyle() const
303 {
304     return m_chatStyle;
305 }
306 
setChatStyle(ChatWindowStyle * chatStyle)307 void AdiumThemeView::setChatStyle(ChatWindowStyle *chatStyle)
308 {
309     m_chatStyle = chatStyle;
310 
311     //load the first variant
312     QHash<QString, QString> variants = chatStyle->getVariants();
313     if (!chatStyle->defaultVariantName().isEmpty()
314             && variants.keys().contains(chatStyle->defaultVariantName())) {
315         m_variantPath = variants.value(chatStyle->defaultVariantName());
316         m_variantName = chatStyle->defaultVariantName();
317     } else if (variants.keys().length() > 0) {
318         m_variantPath = variants.values().first();
319         m_variantName = variants.keys().first();
320     } else {
321         m_variantPath = QLatin1String("");
322         m_variantName = QLatin1String("");
323     }
324 }
325 
fontFamily()326 QString AdiumThemeView::fontFamily()
327 {
328     return m_fontFamily;
329 }
330 
setFontFamily(QString fontFamily)331 void AdiumThemeView::setFontFamily(QString fontFamily)
332 {
333     qCDebug(KTP_TEXTUI_LIB);
334     m_fontFamily = fontFamily;
335 }
336 
fontSize()337 int AdiumThemeView::fontSize()
338 {
339     return m_fontSize;
340 }
341 
setFontSize(int fontSize)342 void AdiumThemeView::setFontSize(int fontSize)
343 {
344     qCDebug(KTP_TEXTUI_LIB);
345     m_fontSize = fontSize;
346 }
347 
setUseCustomFont(bool useCustomFont)348 void AdiumThemeView::setUseCustomFont(bool useCustomFont)
349 {
350     qCDebug(KTP_TEXTUI_LIB);
351     m_useCustomFont = useCustomFont;
352 }
353 
isCustomFont() const354 bool AdiumThemeView::isCustomFont() const
355 {
356     return m_useCustomFont;
357 }
358 
setShowPresenceChanges(bool showPresenceChanges)359 void AdiumThemeView::setShowPresenceChanges(bool showPresenceChanges)
360 {
361     qCDebug(KTP_TEXTUI_LIB);
362     m_showPresenceChanges = showPresenceChanges;
363 }
364 
showPresenceChanges() const365 bool AdiumThemeView::showPresenceChanges() const
366 {
367     return m_showPresenceChanges;
368 }
369 
setShowJoinLeaveChanges(bool showLeaveChanges)370 void AdiumThemeView::setShowJoinLeaveChanges(bool showLeaveChanges)
371 {
372     m_showJoinLeaveChanges = showLeaveChanges;
373 }
374 
showJoinLeaveChanges() const375 bool AdiumThemeView::showJoinLeaveChanges() const
376 {
377     return m_showJoinLeaveChanges;
378 }
379 
isHeaderDisplayed() const380 bool AdiumThemeView::isHeaderDisplayed() const
381 {
382     return m_displayHeader;
383 }
384 
setHeaderDisplayed(bool displayHeader)385 void AdiumThemeView::setHeaderDisplayed(bool displayHeader)
386 {
387     m_displayHeader = displayHeader;
388 }
389 
clear()390 void AdiumThemeView::clear()
391 {
392     if (!page()->url().isEmpty()) {
393         page()->setHtml(QString());
394     }
395 }
396 
addMessage(const KTp::Message & message)397 void AdiumThemeView::addMessage(const KTp::Message &message)
398 {
399     if (message.type() == Tp::ChannelTextMessageTypeAction) {
400         addStatusMessage(QString::fromLatin1("%1 %2").arg(message.senderAlias(), message.mainMessagePart()), message.senderAlias());
401     } else {
402         AdiumThemeContentInfo messageInfo;
403         if (message.direction() == KTp::Message::RemoteToLocal) {
404             if (message.isHistory()) {
405                 messageInfo = AdiumThemeContentInfo(AdiumThemeContentInfo::HistoryRemoteToLocal);
406             } else {
407                 messageInfo = AdiumThemeContentInfo(AdiumThemeContentInfo::RemoteToLocal);
408             }
409         } else {
410             if (message.isHistory()) {
411                 messageInfo = AdiumThemeContentInfo(AdiumThemeContentInfo::HistoryLocalToRemote);
412             } else {
413                 messageInfo = AdiumThemeContentInfo(AdiumThemeContentInfo::LocalToRemote);
414             }
415         }
416 
417         messageInfo.setMessage(message.finalizedMessage());
418         messageInfo.setScript(message.finalizedScript());
419 
420         messageInfo.setTime(message.time());
421 
422         if (message.property("highlight").toBool()) {
423             messageInfo.appendMessageClass(QLatin1String("mention"));
424         }
425         messageInfo.setSenderDisplayName(message.senderAlias());
426         messageInfo.setSenderScreenName(message.senderId());
427         if (message.sender()) {
428             messageInfo.setUserIconPath(message.sender()->avatarData().fileName);
429         }
430 
431         addAdiumContentMessage(messageInfo);
432     }
433 }
434 
addStatusMessage(const QString & text,const QString & sender,const QDateTime & time)435 void AdiumThemeView::addStatusMessage(const QString &text, const QString &sender, const QDateTime &time)
436 {
437     AdiumThemeStatusInfo messageInfo;
438     messageInfo.setMessage(text);
439     messageInfo.setTime(time);
440     messageInfo.setSender(sender);
441 //    messageInfo.setStatus(QLatin1String("error")); //port this?
442     addAdiumStatusMessage(messageInfo);
443 }
444 
addAdiumContentMessage(const AdiumThemeContentInfo & contentMessage)445 void AdiumThemeView::addAdiumContentMessage(const AdiumThemeContentInfo &contentMessage)
446 {
447     QString styleHtml;
448     bool consecutiveMessage = false;
449     bool willAddMoreContentObjects = false; // TODO Find out how this is used in Adium
450     bool replaceLastContent = false; // TODO use this
451 
452     // contentMessage is const, we need a non-const one to append message classes
453     AdiumThemeContentInfo message(contentMessage);
454 
455     // 2 consecutive messages can be combined when:
456     //  * Sender is the same
457     //  * Message type is the same
458     //  * Both have the "mention" class, or none of them have it
459     //  * Theme does not disable consecutive messages
460     if (m_lastContent.senderScreenName() == message.senderScreenName()
461         && m_lastContent.type() == message.type()
462         && m_lastContent.messageClasses().contains(QLatin1String("mention")) == message.messageClasses().contains(QLatin1String("mention"))
463         && !m_chatStyle->disableCombineConsecutive()) {
464         consecutiveMessage = true;
465         message.appendMessageClass(QLatin1String("consecutive"));
466     }
467 
468     m_lastContent = message;
469 
470     switch (message.type()) {
471     case AdiumThemeMessageInfo::RemoteToLocal:
472         if (consecutiveMessage) {
473             styleHtml = m_chatStyle->getIncomingNextContentHtml();
474         } else {
475             styleHtml = m_chatStyle->getIncomingContentHtml();
476         }
477         break;
478     case AdiumThemeMessageInfo::LocalToRemote:
479         if (consecutiveMessage) {
480             styleHtml = m_chatStyle->getOutgoingNextContentHtml();
481         } else {
482             styleHtml = m_chatStyle->getOutgoingContentHtml();
483         }
484         break;
485     case AdiumThemeMessageInfo::HistoryRemoteToLocal:
486         if (consecutiveMessage) {
487             styleHtml = m_chatStyle->getIncomingNextHistoryHtml();
488         } else {
489             styleHtml = m_chatStyle->getIncomingHistoryHtml();
490         }
491         break;
492     case AdiumThemeMessageInfo::HistoryLocalToRemote:
493         if (consecutiveMessage) {
494             styleHtml = m_chatStyle->getOutgoingNextHistoryHtml();
495         } else {
496             styleHtml = m_chatStyle->getOutgoingHistoryHtml();
497         }
498         break;
499     default:
500         qCWarning(KTP_TEXTUI_LIB) << "Unexpected message type to addContentMessage";
501     }
502 
503     replaceContentKeywords(styleHtml, message);
504 
505     AppendMode mode = appendMode(message,
506                                  consecutiveMessage,
507                                  willAddMoreContentObjects,
508                                  replaceLastContent);
509 
510     appendMessage(styleHtml, message.script(), mode);
511 }
512 
addAdiumStatusMessage(const AdiumThemeStatusInfo & statusMessage)513 void AdiumThemeView::addAdiumStatusMessage(const AdiumThemeStatusInfo& statusMessage)
514 {
515     QString styleHtml;
516     bool consecutiveMessage = false;
517     bool willAddMoreContentObjects = false; // TODO Find out how this is used in Adium
518     bool replaceLastContent = false; // TODO use this
519 
520     // statusMessage is const, we need a non-const one to append message classes
521     AdiumThemeStatusInfo message(statusMessage);
522 
523     if (m_lastContent.type() == message.type() && !m_chatStyle->disableCombineConsecutive()) {
524         consecutiveMessage = true;
525         message.appendMessageClass(QLatin1String("consecutive"));
526     }
527 
528     m_lastContent = AdiumThemeContentInfo(statusMessage.type());
529 
530     switch (message.type()) {
531     case AdiumThemeMessageInfo::Status:
532         styleHtml = m_chatStyle->getStatusHtml();
533         break;
534     case AdiumThemeMessageInfo::HistoryStatus:
535         styleHtml = m_chatStyle->getStatusHistoryHtml();
536         break;
537     default:
538         qCWarning(KTP_TEXTUI_LIB) << "Unexpected message type to addStatusMessage";
539     }
540 
541     replaceStatusKeywords(styleHtml, message);
542 
543     AppendMode mode = appendMode(message,
544                                  consecutiveMessage,
545                                  willAddMoreContentObjects,
546                                  replaceLastContent);
547 
548     appendMessage(styleHtml, message.script(), mode);
549 }
550 
appendScript(AdiumThemeView::AppendMode mode)551 QString AdiumThemeView::appendScript(AdiumThemeView::AppendMode mode)
552 {
553     //by making the JS return false runJavaScript is a _lot_ faster, as it has nothing to convert to QVariant.
554     //escape quotes, and merge HTML onto one line.
555     switch (mode) {
556     case AppendMessageWithScroll:
557         qCDebug(KTP_TEXTUI_LIB) << "AppendMessageWithScroll";
558         return QLatin1String("checkIfScrollToBottomIsNeeded(); appendMessage(\"%1\"); scrollToBottomIfNeeded(); false;");
559     case AppendNextMessageWithScroll:
560         qCDebug(KTP_TEXTUI_LIB) << "AppendNextMessageWithScroll";
561         return QLatin1String("checkIfScrollToBottomIsNeeded(); appendNextMessage(\"%1\"); scrollToBottomIfNeeded(); false;");
562     case AppendMessage:
563         qCDebug(KTP_TEXTUI_LIB) << "AppendMessage";
564         return QLatin1String("appendMessage(\"%1\"); false;");
565     case AppendNextMessage:
566         qCDebug(KTP_TEXTUI_LIB) << "AppendNextMessage";
567         return QLatin1String("appendNextMessage(\"%1\"); false;");
568     case AppendMessageNoScroll:
569         qCDebug(KTP_TEXTUI_LIB) << "AppendMessageNoScroll";
570         return QLatin1String("appendMessageNoScroll(\"%1\"); false;");
571     case AppendNextMessageNoScroll:
572         qCDebug(KTP_TEXTUI_LIB) << "AppendNextMessageNoScroll";
573         return QLatin1String("appendNextMessageNoScroll(\"%1\"); false;");
574     case ReplaceLastMessage:
575         qCDebug(KTP_TEXTUI_LIB) << "ReplaceLastMessage";
576         return QLatin1String("replaceLastMessage(\"%1\"); false");
577     default:
578         qCWarning(KTP_TEXTUI_LIB) << "Unhandled append mode!";
579         return QLatin1String("%1");
580     }
581 }
582 
appendMode(const AdiumThemeMessageInfo & message,bool consecutive,bool willAddMoreContentObjects,bool replaceLastContent)583 AdiumThemeView::AppendMode AdiumThemeView::appendMode(const AdiumThemeMessageInfo &message,
584                                                       bool consecutive,
585                                                       bool willAddMoreContentObjects, // TODO Find out how this is used in Adium
586                                                       bool replaceLastContent)
587 {
588     AdiumThemeView::AppendMode mode = AppendModeError;
589     // scripts vary by style version
590     if (!m_chatStyle->hasCustomTemplateHtml() && m_chatStyle->messageViewVersion() >= 4) {
591         // If we're using the built-in template HTML, we know that it supports our most modern scripts
592         if (replaceLastContent)
593             mode = ReplaceLastMessage;
594         else if (willAddMoreContentObjects) {
595             mode = (consecutive ? AppendNextMessageNoScroll : AppendMessageNoScroll);
596         } else {
597             mode = (consecutive ? AppendNextMessage : AppendMessage);
598         }
599     } else  if (m_chatStyle->messageViewVersion() >= 3) {
600         if (willAddMoreContentObjects) {
601             mode = (consecutive ? AppendNextMessageNoScroll : AppendMessageNoScroll);
602         } else {
603             mode = (consecutive ? AppendNextMessage : AppendMessage);
604         }
605     } else if (m_chatStyle->messageViewVersion() >= 1) {
606         mode = (consecutive ? AppendNextMessage : AppendMessage);
607     } else if (m_chatStyle->hasCustomTemplateHtml() && (message.type() == AdiumThemeContentInfo::Status ||
608                                                         message.type() == AdiumThemeContentInfo::HistoryStatus)) {
609         // Old styles with a custom Template.html had Status.html files without 'insert' divs coupled
610         // with a APPEND_NEXT_MESSAGE_WITH_SCROLL script which assumes one exists.
611         mode = AppendMessageWithScroll;
612     } else {
613         mode = (consecutive ? AppendNextMessageWithScroll : AppendMessageWithScroll);
614     }
615 
616     return mode;
617 }
618 
appendMessage(QString & html,const QString & script,AppendMode mode)619 void AdiumThemeView::appendMessage(QString &html, const QString &script, AppendMode mode)
620 {
621     QString js = appendScript(mode).arg(html.replace(QLatin1Char('\\'), QLatin1String("\\\\")) /* replace single \ with \\   */
622                                             .replace(QLatin1Char('\"'), QLatin1String("\\\"")) /* replace " with \"   */
623                                             .replace(QLatin1Char('\n'), QLatin1String(""))); /* remove new lines    */
624 
625     page()->runJavaScript(js);
626 
627     if (!script.isEmpty()) {
628         page()->runJavaScript(script);
629     }
630 }
631 
632 /** Private */
633 
replaceHeaderKeywords(QString htmlTemplate,const AdiumThemeHeaderInfo & info)634 QString AdiumThemeView::replaceHeaderKeywords(QString htmlTemplate, const AdiumThemeHeaderInfo & info)
635 {
636     htmlTemplate.replace(QLatin1String("%chatName%"), info.chatName());
637     htmlTemplate.replace(QLatin1String("%topic%"), info.chatName());
638     htmlTemplate.replace(QLatin1String("%sourceName%"), info.sourceName());
639     htmlTemplate.replace(QLatin1String("%destinationName%"), info.destinationName());
640     htmlTemplate.replace(QLatin1String("%destinationDisplayName%"), info.destinationDisplayName());
641     htmlTemplate.replace(QLatin1String("%incomingIconPath%"), (!info.incomingIconPath().isEmpty() ? info.incomingIconPath().toString() : m_defaultAvatar));
642     htmlTemplate.replace(QLatin1String("%outgoingIconPath%"), (!info.outgoingIconPath().isEmpty() ? info.outgoingIconPath().toString() : m_defaultAvatar));
643     htmlTemplate.replace(QLatin1String("%timeOpened%"), QLocale::system().toString(info.timeOpened().time()));
644     htmlTemplate.replace(QLatin1String("%dateOpened%"), QLocale::system().toString(info.timeOpened().date(), QLocale::LongFormat));
645 
646     //KTp-Renkoo specific hack to make "Conversation Began" translatable
647     htmlTemplate.replace(QLatin1String("%conversationBegan%"), i18nc("Header at top of conversation view. %1 is the time format",
648                                                                      "Conversation began %1", QLocale::system().toString(info.timeOpened().time())));
649 
650     //KTp-WoshiChat specific hack to make "Joined at" translatable
651     htmlTemplate.replace(QLatin1String("%conversationJoined%"), i18nc("Header at top of conversation view. %1 is the time format",
652                                                                       "Joined at %1", QLocale::system().toString(info.timeOpened().time())));
653 
654     htmlTemplate.replace(QLatin1String("%groupChatIcon%"), KIconLoader::global()->iconPath(QLatin1String("telepathy-kde"), -48));
655 
656     //FIXME time fields - remember to do both, steal the complicated one from Kopete code.
657     // Look for %timeOpened{X}%
658     QRegExp timeRegExp(QLatin1String("%timeOpened\\{([^}]*)\\}%"));
659     int pos = 0;
660     while ((pos = timeRegExp.indexIn(htmlTemplate , pos)) != -1) {
661         QString timeKeyword = formatTime(timeRegExp.cap(1), info.timeOpened());
662         htmlTemplate.replace(pos , timeRegExp.cap(0).length() , timeKeyword);
663     }
664     htmlTemplate.replace(QLatin1String("%service%"), info.service());
665     htmlTemplate.replace(QLatin1String("%serviceIconPath%"), info.serviceIconPath());
666     htmlTemplate.replace(QLatin1String("%serviceIconImg%"),
667                          QString::fromLatin1("<img src=\"%1\" class=\"serviceIcon\" />").arg(info.serviceIconPath()));
668     return htmlTemplate;
669 }
670 
replaceContentKeywords(QString & htmlTemplate,const AdiumThemeContentInfo & info)671 QString AdiumThemeView::replaceContentKeywords(QString& htmlTemplate, const AdiumThemeContentInfo& info)
672 {
673     //userIconPath
674     htmlTemplate.replace(QLatin1String("%userIconPath%"), !info.userIconPath().isEmpty() ? info.userIconPath() : m_defaultAvatar);
675     //senderScreenName
676     htmlTemplate.replace(QLatin1String("%senderScreenName%"), info.senderScreenName());
677     //sender
678     htmlTemplate.replace(QLatin1String("%sender%"), info.sender());
679     //senderColor
680     htmlTemplate.replace(QLatin1String("%senderColor%"), info.senderColor());
681     //senderStatusIcon
682     htmlTemplate.replace(QLatin1String("%senderStatusIcon%"), info.senderStatusIcon());
683     //senderDisplayName
684     htmlTemplate.replace(QLatin1String("%senderDisplayName%"), info.senderDisplayName());
685     //Few themes use this and it is IRC specific. It is also undocumented
686     //see https://bugs.kde.org/show_bug.cgi?id=316323 for details
687     //simply replace with an empty string
688     htmlTemplate.replace(QLatin1String("%senderPrefix%"), QString());
689 
690     //FIXME %textbackgroundcolor{X}%
691     return replaceMessageKeywords(htmlTemplate, info);
692 }
693 
replaceStatusKeywords(QString & htmlTemplate,const AdiumThemeStatusInfo & info)694 QString AdiumThemeView::replaceStatusKeywords(QString &htmlTemplate, const AdiumThemeStatusInfo& info)
695 {
696     // status
697     htmlTemplate.replace(QLatin1String("%status%"), info.status());
698     // sender
699     htmlTemplate.replace(QLatin1String("%sender%"), info.sender());
700 
701     return replaceMessageKeywords(htmlTemplate, info);
702 }
703 
replaceMessageKeywords(QString & htmlTemplate,const AdiumThemeMessageInfo & info)704 QString AdiumThemeView::replaceMessageKeywords(QString &htmlTemplate, const AdiumThemeMessageInfo& info)
705 {
706     //message
707     QString message = info.message();
708 
709     if(info.messageDirection() == QLatin1String("rtl")) {
710         message.prepend(QString::fromLatin1("<div dir=\"rtl\">"));
711         message.append(QLatin1String("</div>"));
712     }
713 
714     htmlTemplate.replace(QLatin1String("%message%"), message);
715 
716     //service
717     htmlTemplate.replace(QLatin1String("%service%"), m_service);
718     //time
719     htmlTemplate.replace(QLatin1String("%time%"), QLocale::system().toString(info.time().time()));
720     //shortTime
721     htmlTemplate.replace(QLatin1String("%shortTime%"), QLocale::system().toString(info.time().time(), QLocale::ShortFormat));
722     //time{X}
723     QRegExp timeRegExp(QLatin1String("%time\\{([^}]*)\\}%"));
724     int pos = 0;
725     while ((pos = timeRegExp.indexIn(htmlTemplate , pos)) != -1) {
726         QString timeKeyword = formatTime(timeRegExp.cap(1), info.time());
727         htmlTemplate.replace(pos , timeRegExp.cap(0).length() , timeKeyword);
728     }
729 
730     //messageDirection
731     htmlTemplate.replace(QLatin1String("%messageDirection%"), info.messageDirection());
732     htmlTemplate.replace(QLatin1String("%messageClasses%"), info.messageClasses());
733 
734 
735     return htmlTemplate;
736 }
737 
738 //taken from Kopete code
formatTime(const QString & timeFormat,const QDateTime & dateTime)739 QString AdiumThemeView::formatTime(const QString &timeFormat, const QDateTime &dateTime)
740 {
741     QString format = timeFormat;
742 
743     // see "man date"
744 
745     // Just discard the modifiers
746     format.replace(QLatin1String("%-"), QLatin1String("%")); // (hyphen) do not pad the field
747     format.replace(QLatin1String("%_"), QLatin1String("%")); // (underscore) pad with spaces
748     format.replace(QLatin1String("%0"), QLatin1String("%")); // (zero) pad with zeros
749     format.replace(QLatin1String("%^"), QLatin1String("%")); // use upper case if possible
750     format.replace(QLatin1String("%#"), QLatin1String("%")); // use opposite case if possible
751 
752     // Now do the real replacement
753     format.replace(QLatin1String("%a"), QLatin1String("ddd"));        // locale's abbreviated weekday name (e.g., Sun)
754     format.replace(QLatin1String("%A"), QLatin1String("dddd"));       // locale's full weekday name (e.g., Sunday)
755     format.replace(QLatin1String("%b"), QLatin1String("MMM"));        // locale's abbreviated month name (e.g., Jan)
756     format.replace(QLatin1String("%B"), QLatin1String("MMMM"));       // locale's full month name (e.g., January)
757     format.replace(QLatin1String("%c"), QLatin1String("ddd MMM d hh:mm:ss yyyy")); // FIXME locale's date and time (e.g., Thu Mar  3 23:05:25 2005)
758     format.replace(QLatin1String("%C"), QLatin1String(""));           // FIXME century; like %Y, except omit last two digits (e.g., 20)
759     format.replace(QLatin1String("%d"), QLatin1String("dd"));         // day of month (e.g., 01)
760     format.replace(QLatin1String("%D"), QLatin1String("MM/dd/yy"));   // date; same as %m/%d/%y
761     format.replace(QLatin1String("%e"), QLatin1String("d"));          // FIXME day of month, space padded; same as %_d
762     format.replace(QLatin1String("%F"), QLatin1String("yyyy-MM-dd")); // full date; same as %Y-%m-%d
763     format.replace(QLatin1String("%g"), QLatin1String(""));           // FIXME last two digits of year of ISO week number (see %G)
764     format.replace(QLatin1String("%G"), QLatin1String(""));           // year of ISO week number (see %V); normally useful only with %V
765     format.replace(QLatin1String("%h"), QLatin1String("MMM"));        // same as %b
766     format.replace(QLatin1String("%H"), QLatin1String("HH"));         // hour (00..23)
767     format.replace(QLatin1String("%I"), QLatin1String("hh"));         // FIXME hour (01..12)
768     format.replace(QLatin1String("%j"), QLatin1String(""));           // FIXME day of year (001..366)
769     format.replace(QLatin1String("%k"), QLatin1String("H"));          // hour, space padded ( 0..23); same as %_H
770     format.replace(QLatin1String("%l"), QLatin1String("h"));          // hour, space padded ( 0..23); same as %_H
771     format.replace(QLatin1String("%m"), QLatin1String("MM"));         // month (01..12)
772     format.replace(QLatin1String("%M"), QLatin1String("mm"));         // minute (00..59)
773     format.replace(QLatin1String("%n"), QLatin1String("\n"));         // a newline
774     format.replace(QLatin1String("%N"), QLatin1String("zzz"));        // FIXME nanoseconds (000000000..999999999)
775     format.replace(QLatin1String("%p"), QLatin1String("AP"));         // locale's equivalent of either AM or PM; blank if not known
776     format.replace(QLatin1String("%P"), QLatin1String("ap"));         // like %p, but lower case
777     format.replace(QLatin1String("%r"), QLatin1String("hh:mm:ss AP")); // FIXME locale's 12-hour clock time (e.g., 11:11:04 PM)
778     format.replace(QLatin1String("%R"), QLatin1String("HH:mm"));      // 24-hour hour and minute; same as %H:%M
779     format.replace(QLatin1String("%s"), QLatin1String(""));           // FIXME seconds since 1970-01-01 00:00:00 UTC
780     format.replace(QLatin1String("%S"), QLatin1String("ss"));         // second (00..60)
781     format.replace(QLatin1String("%t"), QLatin1String("\t"));         // a tab
782     format.replace(QLatin1String("%T"), QLatin1String("HH:mm:ss"));   // time; same as %H:%M:%S
783     format.replace(QLatin1String("%u"), QLatin1String(""));           // FIXME day of week (1..7); 1 is Monday
784     format.replace(QLatin1String("%U"), QLatin1String(""));           // FIXME week number of year, with Sunday as first day of week (00..53)
785     format.replace(QLatin1String("%V"), QLatin1String(""));           // FIXME ISO week number, with Monday as first day of week (01..53)
786     format.replace(QLatin1String("%w"), QLatin1String(""));           // FIXME day of week (0..6); 0 is Sunday
787     format.replace(QLatin1String("%W"), QLatin1String(""));           // FIXME week number of year, with Monday as first day of week (00..53)
788     format.replace(QLatin1String("%x"), QLatin1String("MM/dd/yy"));   // FIXME locale's date representation (e.g., 12/31/99)
789     format.replace(QLatin1String("%x"), QLatin1String("HH:mm:ss"));   // FIXME locale's time representation (e.g., 23:13:48)
790     format.replace(QLatin1String("%y"), QLatin1String("yy"));         // last two digits of year (00..99)
791     format.replace(QLatin1String("%Y"), QLatin1String("yyyy"));       // year
792     format.replace(QLatin1String("%z"), QLatin1String(""));           // FIXME +hhmm numeric time zone (e.g., -0400)
793     format.replace(QLatin1String("%:z"), QLatin1String(""));          // FIXME +hh:mm numeric time zone (e.g., -04:00)
794     format.replace(QLatin1String("%::z"), QLatin1String(""));         // FIXME +hh:mm::ss numeric time zone (e.g., -04:00:00)
795     format.replace(QLatin1String("%:::z"), QLatin1String(""));        // FIXME numeric time zone with : to necessary precision (e.g., -04, +05:30)
796     format.replace(QLatin1String("%Z"), QLatin1String(""));           // FIXME alphabetic time zone abbreviation (e.g., EDT)
797 
798     // Last is the literal %
799     format.replace(QLatin1String("%%"), QLatin1String("%"));          // a literal %
800 
801     return dateTime.toString(format);
802 }
803 
variantName() const804 const QString AdiumThemeView::variantName() const
805 {
806     return m_variantName;
807 }
808 
variantPath() const809 const QString AdiumThemeView::variantPath() const
810 {
811     return m_variantPath;
812 }
813