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