diff options
| -rw-r--r-- | BuildTools/Coverage/.gitignore | 1 | ||||
| -rw-r--r-- | QA/UnitTest/SConscript | 1 | ||||
| -rw-r--r-- | SConstruct | 13 | ||||
| -rw-r--r-- | SwifTools/Linkify.cpp | 17 | ||||
| -rw-r--r-- | SwifTools/Linkify.h | 9 | ||||
| -rw-r--r-- | SwifTools/SConscript | 15 | ||||
| -rw-r--r-- | SwifTools/UnitTest/LinkifyTest.cpp | 60 | ||||
| -rw-r--r-- | SwifTools/UnitTest/SConscript | 5 | ||||
| -rw-r--r-- | Swift/QtUI/QtChatView.cpp | 6 | ||||
| -rw-r--r-- | Swift/QtUI/QtChatView.h | 2 | ||||
| -rw-r--r-- | Swift/QtUI/QtChatWindow.cpp | 2 | ||||
| -rw-r--r-- | Swift/QtUI/SConscript | 1 |
12 files changed, 129 insertions, 3 deletions
diff --git a/BuildTools/Coverage/.gitignore b/BuildTools/Coverage/.gitignore new file mode 100644 index 0000000..1a06816 --- /dev/null +++ b/BuildTools/Coverage/.gitignore @@ -0,0 +1 @@ +results diff --git a/QA/UnitTest/SConscript b/QA/UnitTest/SConscript index f4bb358..2fd7ce0 100644 --- a/QA/UnitTest/SConscript +++ b/QA/UnitTest/SConscript @@ -1,26 +1,27 @@ import os Import("env") if env["TEST"] : myenv = env.Clone() myenv.MergeFlags(env["CHECKER_FLAGS"]) myenv.MergeFlags(env["SLIMBER_FLAGS"]) myenv.MergeFlags(env["SWIFT_CONTROLLERS_FLAGS"]) + myenv.MergeFlags(env["SWIFTOOLS_FLAGS"]) myenv.MergeFlags(env["SWIFTEN_FLAGS"]) myenv.MergeFlags(env["CPPUNIT_FLAGS"]) myenv.MergeFlags(env["LIBIDN_FLAGS"]) myenv.MergeFlags(env["BOOST_FLAGS"]) myenv.MergeFlags(env["SQLITE_FLAGS"]) myenv.MergeFlags(env.get("LIBXML_FLAGS", "")) myenv.MergeFlags(env.get("EXPAT_FLAGS", "")) myenv.MergeFlags(env["ZLIB_FLAGS"]) if env.get("HAVE_LIBXML") : myenv.Append(CPPDEFINES = ["HAVE_LIBXML"]) if env.get("HAVE_EXPAT") : myenv.Append(CPPDEFINES = ["HAVE_EXPAT"]) checker = myenv.Program("checker", env["UNITTEST_SOURCES"]) for i in ["HOME", "USERPROFILE", "APPDATA"]: if os.environ.get(i, "") : myenv["ENV"][i] = os.environ[i] myenv.Test(checker) @@ -209,94 +209,101 @@ if env["qt"] : # OpenSSL openssl_env = conf_env.Clone() use_openssl = bool(env["openssl"]) openssl_prefix = env["openssl"] if isinstance(env["openssl"], str) else "" openssl_flags = {} if openssl_prefix : openssl_flags = { "CPPPATH": [os.path.join(openssl_prefix, "include")] } if env["PLATFORM"] == "win32" : openssl_flags["LIBPATH"] = [os.path.join(openssl_prefix, "lib", "VC")] env["OPENSSL_DIR"] = openssl_prefix else : openssl_flags["LIBPATH"] = [os.path.join(openssl_prefix, "lib")] openssl_env.MergeFlags(openssl_flags) openssl_conf = Configure(openssl_env) if use_openssl and openssl_conf.CheckCHeader("openssl/ssl.h") : env["HAVE_OPENSSL"] = 1 env["OPENSSL_FLAGS"] = openssl_flags if env["PLATFORM"] == "win32" : env["OPENSSL_FLAGS"]["LIBS"] = ["libeay32MT", "ssleay32MT"] else: env["OPENSSL_FLAGS"]["LIBS"] = ["ssl", "crypto"] else : env["OPENSSL_FLAGS"] = "" openssl_conf.Finish() # Bonjour if env["PLATFORM"] == "darwin" : env["HAVE_BONJOUR"] = 1 elif env.get("bonjour", False) : bonjour_env = conf_env.Clone() bonjour_conf = Configure(bonjour_env) bonjour_flags = {} if env.get("bonjour") != True : bonjour_prefix = env["bonjour"] bonjour_flags["CPPPATH"] = [os.path.join(bonjour_prefix, "include")] bonjour_flags["LIBPATH"] = [os.path.join(bonjour_prefix, "lib", "win32")] bonjour_env.MergeFlags(bonjour_flags) if bonjour_conf.CheckCHeader("dns_sd.h") and bonjour_conf.CheckLib("dnssd") : env["HAVE_BONJOUR"] = 1 env["BONJOUR_FLAGS"] = bonjour_flags env["BONJOUR_FLAGS"]["LIBS"] = ["dnssd"] bonjour_conf.Finish() ################################################################################ # Project files +# FIXME: We need to explicitly list the order of libraries here, because of +# the exported FLAGS. We should put FLAGS in separate SConscript files, and +# read these in before anything else, such that we don't need to manually +# list modules in order. ################################################################################ # Third-party modules SConscript(dirs = [ "3rdParty/CppUnit", "3rdParty/Boost", "3rdParty/LibIDN", "3rdParty/SQLite"]) # Checker SConscript(dirs = ["QA/Checker"]) -# Swiften -SConscript(dirs = "Swiften") +# Libraries +SConscript(dirs = [ + "Swiften", + "SwifTools" + ]) # Projects for dir in os.listdir(".") : - if dir in ["QA", "Swiften"] : + if dir in ["QA", "Swiften", "SwifTools"] : continue sconscript = os.path.join(dir, "SConscript") if os.path.isfile(sconscript) : SConscript(sconscript) # Unit test runner SConscript(dirs = ["QA/UnitTest"]) ################################################################################ # Print summary ################################################################################ print print " Build Configuration" print " -------------------" parsers = [] if env.get("HAVE_LIBXML", 0): parsers.append("LibXML") if env.get("HAVE_EXPAT", 0): parsers.append("Expat") if bundledExpat: parsers.append("(Bundled)") print " XML Parsers: " + ' '.join(parsers) print " TLS Support: " + ("OpenSSL" if env.get("HAVE_OPENSSL",0) else "Disabled") print " DNSSD Support: " + ("Bonjour" if env.get("HAVE_BONJOUR") else ("Avahi" if env.get("HAVE_AVAHI") else "Disabled")) print diff --git a/SwifTools/Linkify.cpp b/SwifTools/Linkify.cpp new file mode 100644 index 0000000..8654307 --- /dev/null +++ b/SwifTools/Linkify.cpp @@ -0,0 +1,17 @@ +#include "SwifTools/Linkify.h" + +#include <boost/regex.hpp> + +namespace Swift { + +static const boost::regex linkifyRegexp("(https?://([-\\w\\.]+)+(:\\d+)?(/([%-\\w/_\\.]*(\\?\\S+)?)?)?)"); + +String Linkify::linkify(const String& input) { + return String(boost::regex_replace( + input.getUTF8String(), + linkifyRegexp, + "<a href=\"\\1\">\\1</a>", + boost::match_default|boost::format_all)); +} + +} diff --git a/SwifTools/Linkify.h b/SwifTools/Linkify.h new file mode 100644 index 0000000..04182f9 --- /dev/null +++ b/SwifTools/Linkify.h @@ -0,0 +1,9 @@ +#pragma once + +#include "Swiften/Base/String.h" + +namespace Swift { + namespace Linkify { + String linkify(const String&); + } +} diff --git a/SwifTools/SConscript b/SwifTools/SConscript new file mode 100644 index 0000000..2caff5f --- /dev/null +++ b/SwifTools/SConscript @@ -0,0 +1,15 @@ +Import("env") + +env["SWIFTOOLS_FLAGS"] = { + "LIBPATH": [Dir(".")], + "LIBS": ["SwifTools"] + } + +myenv = env.Clone() + +myenv.MergeFlags(myenv["BOOST_FLAGS"]) +myenv.StaticLibrary("SwifTools", [ + "Linkify.cpp" + ]) + +SConscript(dirs = ["UnitTest"]) diff --git a/SwifTools/UnitTest/LinkifyTest.cpp b/SwifTools/UnitTest/LinkifyTest.cpp new file mode 100644 index 0000000..9b66614 --- /dev/null +++ b/SwifTools/UnitTest/LinkifyTest.cpp @@ -0,0 +1,60 @@ +#include <cppunit/extensions/HelperMacros.h> +#include <cppunit/extensions/TestFactoryRegistry.h> + +#include "SwifTools/Linkify.h" + +using namespace Swift; + +class LinkifyTest : public CppUnit::TestFixture { + CPPUNIT_TEST_SUITE(LinkifyTest); + CPPUNIT_TEST(testLinkify_URLWithResource); + CPPUNIT_TEST(testLinkify_URLWithEmptyResource); + CPPUNIT_TEST(testLinkify_BareURL); + CPPUNIT_TEST(testLinkify_URLSurroundedByWhitespace); + CPPUNIT_TEST(testLinkify_MultipleURLs); + CPPUNIT_TEST_SUITE_END(); + + public: + void testLinkify_URLWithResource() { + String result = Linkify::linkify("http://swift.im/blog"); + + CPPUNIT_ASSERT_EQUAL( + String("<a href=\"http://swift.im/blog\">http://swift.im/blog</a>"), + result); + } + + void testLinkify_URLWithEmptyResource() { + String result = Linkify::linkify("http://swift.im/"); + + CPPUNIT_ASSERT_EQUAL( + String("<a href=\"http://swift.im/\">http://swift.im/</a>"), + result); + } + + + void testLinkify_BareURL() { + String result = Linkify::linkify("http://swift.im"); + + CPPUNIT_ASSERT_EQUAL( + String("<a href=\"http://swift.im\">http://swift.im</a>"), + result); + } + + void testLinkify_URLSurroundedByWhitespace() { + String result = Linkify::linkify("Foo http://swift.im/blog Bar"); + + CPPUNIT_ASSERT_EQUAL( + String("Foo <a href=\"http://swift.im/blog\">http://swift.im/blog</a> Bar"), + result); + } + + void testLinkify_MultipleURLs() { + String result = Linkify::linkify("Foo http://swift.im/blog Bar http://el-tramo.be/about Baz"); + + CPPUNIT_ASSERT_EQUAL( + String("Foo <a href=\"http://swift.im/blog\">http://swift.im/blog</a> Bar <a href=\"http://el-tramo.be/about\">http://el-tramo.be/about</a> Baz"), + result); + } +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(LinkifyTest); diff --git a/SwifTools/UnitTest/SConscript b/SwifTools/UnitTest/SConscript new file mode 100644 index 0000000..2622f39 --- /dev/null +++ b/SwifTools/UnitTest/SConscript @@ -0,0 +1,5 @@ +Import("env") + +env.Append(UNITTEST_SOURCES = [ + File("LinkifyTest.cpp") + ]) diff --git a/Swift/QtUI/QtChatView.cpp b/Swift/QtUI/QtChatView.cpp index 0a02591..12f6beb 100644 --- a/Swift/QtUI/QtChatView.cpp +++ b/Swift/QtUI/QtChatView.cpp @@ -1,91 +1,97 @@ #include "QtChatView.h" #include <QtDebug> #include <QFile> +#include <QDesktopServices> #include <QVBoxLayout> #include <QWebView> #include <QWebFrame> #include <QKeyEvent> #include <QStackedWidget> namespace Swift { QtChatView::QtChatView(QWidget* parent) : QWidget(parent) { setFocusPolicy(Qt::NoFocus); QVBoxLayout* mainLayout = new QVBoxLayout(this); mainLayout->setSpacing(0); mainLayout->setContentsMargins(0,0,0,0); webView_ = new QWebView(this); webView_->setFocusPolicy(Qt::NoFocus); + connect(webView_, SIGNAL(linkClicked(const QUrl&)), SLOT(handleLinkClicked(const QUrl&))); #ifdef Q_WS_X11 /* To give a border on Linux, where it looks bad without */ QStackedWidget* stack = new QStackedWidget(this); stack->addWidget(webView_); stack->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken); stack->setLineWidth(2); mainLayout->addWidget(stack); #else mainLayout->addWidget(webView_); #endif webPage_ = new QWebPage(this); webPage_->setLinkDelegationPolicy(QWebPage::DelegateAllLinks); webView_->setPage(webPage_); connect(webPage_, SIGNAL(selectionChanged()), SLOT(copySelectionToClipboard())); QFile file(":/themes/Default/Template.html"); bool result = file.open(QIODevice::ReadOnly); Q_ASSERT(result); Q_UNUSED(result); QString pageHTML = file.readAll(); pageHTML.replace("==bodyBackground==", "background-color:#e3e3e3"); pageHTML.replace(pageHTML.indexOf("%@"), 2, "qrc:/themes/Default/"); pageHTML.replace(pageHTML.indexOf("%@"), 2, "Variants/Blue on Green.css"); pageHTML.replace(pageHTML.indexOf("%@"), 2, ""); pageHTML.replace(pageHTML.indexOf("%@"), 2, ""); file.close(); webPage_->mainFrame()->setHtml(pageHTML); } void QtChatView::addMessage(const ChatSnippet& snippet) { //bool wasScrolledToBottom = isScrolledToBottom(); QString content = snippet.getContent(); content.replace("\\", "\\\\"); content.replace("\"", "\\\""); content.replace("\n", "\\n"); content.replace("\r", ""); if (previousContinuationElementID_.isEmpty() || !snippet.getAppendToPrevious()) { webPage_->mainFrame()->evaluateJavaScript("appendMessage(\"" + content + "\");"); } else { webPage_->mainFrame()->evaluateJavaScript("appendNextMessage(\"" + content + "\");"); } //qDebug() << webPage_->mainFrame()->toHtml(); previousContinuationElementID_ = snippet.getContinuationElementID(); /*if (wasScrolledToBottom) { scrollToBottom(); }*/ } void QtChatView::copySelectionToClipboard() { if (!webPage_->selectedText().isEmpty()) { webPage_->triggerAction(QWebPage::Copy); } } bool QtChatView::isScrolledToBottom() const { return webPage_->mainFrame()->scrollBarValue(Qt::Vertical) == webPage_->mainFrame()->scrollBarMaximum(Qt::Vertical); } void QtChatView::scrollToBottom() { webPage_->mainFrame()->setScrollBarValue(Qt::Vertical, webPage_->mainFrame()->scrollBarMaximum(Qt::Vertical)); } +void QtChatView::handleLinkClicked(const QUrl& url) { + QDesktopServices::openUrl(url); +} + } diff --git a/Swift/QtUI/QtChatView.h b/Swift/QtUI/QtChatView.h index 2a50129..7340e00 100644 --- a/Swift/QtUI/QtChatView.h +++ b/Swift/QtUI/QtChatView.h @@ -1,32 +1,34 @@ #ifndef SWIFT_QtChatView_H #define SWIFT_QtChatView_H #include <QString> #include <QWidget> #include "ChatSnippet.h" class QWebView; class QWebPage; +class QUrl; namespace Swift { class QtChatView : public QWidget { Q_OBJECT public: QtChatView(QWidget* parent); void addMessage(const ChatSnippet& snippet); bool isScrolledToBottom() const; public slots: void copySelectionToClipboard(); void scrollToBottom(); + void handleLinkClicked(const QUrl&); private: QWebView* webView_; QWebPage* webPage_; QString previousContinuationElementID_; }; } #endif diff --git a/Swift/QtUI/QtChatWindow.cpp b/Swift/QtUI/QtChatWindow.cpp index fc8dc1e..bebebe8 100644 --- a/Swift/QtUI/QtChatWindow.cpp +++ b/Swift/QtUI/QtChatWindow.cpp @@ -1,52 +1,53 @@ #include "QtChatWindow.h" #include "QtSwiftUtil.h" #include "Roster/QtTreeWidget.h" #include "Roster/QtTreeWidgetFactory.h" +#include "SwifTools/Linkify.h" #include "QtChatView.h" #include "MessageSnippet.h" #include "SystemMessageSnippet.h" #include "QtTextEdit.h" #include <QApplication> #include <QBoxLayout> #include <QCloseEvent> #include <QComboBox> #include <QLineEdit> #include <QSplitter> #include <QString> #include <QTextEdit> #include <QTime> #include <QUrl> namespace Swift { QtChatWindow::QtChatWindow(const QString &contact, QtTreeWidgetFactory *treeWidgetFactory) : QtTabbable(), contact_(contact), previousMessageWasSelf_(false), previousMessageWasSystem_(false) { unreadCount_ = 0; updateTitleWithUnreadCount(); QBoxLayout *layout = new QBoxLayout(QBoxLayout::TopToBottom, this); layout->setContentsMargins(0,0,0,0); layout->setSpacing(2); QSplitter *logRosterSplitter = new QSplitter(this); layout->addWidget(logRosterSplitter); messageLog_ = new QtChatView(this); logRosterSplitter->addWidget(messageLog_); treeWidget_ = dynamic_cast<QtTreeWidget*>(treeWidgetFactory->createTreeWidget()); treeWidget_->hide(); logRosterSplitter->addWidget(treeWidget_); QWidget* midBar = new QWidget(this); layout->addWidget(midBar); QHBoxLayout *midBarLayout = new QHBoxLayout(midBar); midBarLayout->setContentsMargins(0,0,0,0); midBarLayout->setSpacing(2); midBarLayout->addStretch(); labelsWidget_ = new QComboBox(this); labelsWidget_->setFocusPolicy(Qt::NoFocus); labelsWidget_->hide(); labelsWidget_->setSizeAdjustPolicy(QComboBox::AdjustToContents); midBarLayout->addWidget(labelsWidget_,0); @@ -110,96 +111,97 @@ void QtChatWindow::convertToMUC() { treeWidget_->show(); } void QtChatWindow::qAppFocusChanged(QWidget *old, QWidget *now) { Q_UNUSED(old); Q_UNUSED(now); if (isWidgetSelected()) { onAllMessagesRead(); } } void QtChatWindow::setInputEnabled(bool enabled) { input_->setEnabled(enabled); } void QtChatWindow::showEvent(QShowEvent* event) { emit windowOpening(); QWidget::showEvent(event); } void QtChatWindow::setUnreadMessageCount(int count) { unreadCount_ = count; updateTitleWithUnreadCount(); } void QtChatWindow::setName(const String& name) { contact_ = P2QSTRING(name); updateTitleWithUnreadCount(); } void QtChatWindow::updateTitleWithUnreadCount() { setWindowTitle(unreadCount_ > 0 ? QString("(%1) %2").arg(unreadCount_).arg(contact_) : contact_); emit titleUpdated(); } void QtChatWindow::addMessage(const String &message, const String &senderName, bool senderIsSelf, const boost::optional<SecurityLabel>& label, const String& avatarPath) { if (isWidgetSelected()) { onAllMessagesRead(); } QString htmlString; if (label) { htmlString = QString("<span style=\"border: thin dashed grey; padding-left: .5em; padding-right: .5em; color: %1; background-color: %2; font-size: 90%; margin-right: .5em; \">").arg(Qt::escape(P2QSTRING(label->getForegroundColor()))).arg(Qt::escape(P2QSTRING(label->getBackgroundColor()))); htmlString += QString("%3</span> ").arg(Qt::escape(P2QSTRING(label->getDisplayMarking()))); } QString messageHTML(Qt::escape(P2QSTRING(message))); messageHTML.replace("\n","<br/>"); + messageHTML = P2QSTRING(Linkify::linkify(Q2PSTRING(messageHTML))); htmlString += messageHTML; bool appendToPrevious = !previousMessageWasSystem_ && ((senderIsSelf && previousMessageWasSelf_) || (!senderIsSelf && !previousMessageWasSelf_ && previousSenderName_ == P2QSTRING(senderName))); QString qAvatarPath = avatarPath.isEmpty() ? "qrc:/icons/avatar.png" : QUrl::fromLocalFile(P2QSTRING(avatarPath)).toEncoded(); messageLog_->addMessage(MessageSnippet(htmlString, Qt::escape(P2QSTRING(senderName)), QDateTime::currentDateTime(), qAvatarPath, senderIsSelf, appendToPrevious)); previousMessageWasSelf_ = senderIsSelf; previousSenderName_ = P2QSTRING(senderName); previousMessageWasSystem_ = false; } void QtChatWindow::addErrorMessage(const String& errorMessage) { if (isWidgetSelected()) { onAllMessagesRead(); } QString errorMessageHTML(Qt::escape(P2QSTRING(errorMessage))); errorMessageHTML.replace("\n","<br/>"); messageLog_->addMessage(SystemMessageSnippet(QString("<span class=\"error\">%1</span>").arg(errorMessageHTML), QDateTime::currentDateTime(),previousMessageWasSystem_)); previousMessageWasSelf_ = false; previousMessageWasSystem_ = true; } void QtChatWindow::addSystemMessage(const String& message) { if (isWidgetSelected()) { onAllMessagesRead(); } QString messageHTML(Qt::escape(P2QSTRING(message))); messageHTML.replace("\n","<br/>"); messageLog_->addMessage(SystemMessageSnippet(messageHTML, QDateTime::currentDateTime(),previousMessageWasSystem_)); previousMessageWasSelf_ = false; previousMessageWasSystem_ = true; } void QtChatWindow::returnPressed() { onSendMessageRequest(Q2PSTRING(input_->toPlainText())); messageLog_->scrollToBottom(); input_->clear(); } void QtChatWindow::show() { QWidget::show(); emit windowOpening(); } diff --git a/Swift/QtUI/SConscript b/Swift/QtUI/SConscript index ee1c762..d30f3b9 100644 --- a/Swift/QtUI/SConscript +++ b/Swift/QtUI/SConscript @@ -1,71 +1,72 @@ import os, shutil, datetime import Version def generateDefaultTheme(env, target, source) : sourceDir = source[0].abspath output = open(target[0].abspath, "w") output.write("<RCC version =\"1.0\">") output.write("<qresource prefix=\"/themes/Default\">") for (path, dirs, files) in os.walk(sourceDir) : for file in files : filePath = os.path.join(path,file) output.write("<file alias=\"%(alias)s\">%(path)s</file>" % { "alias": filePath[len(sourceDir)+1:], "path": filePath }) output.write("</qresource>") output.write("</RCC>") Import("env") myenv = env.Clone() myenv.MergeFlags(env["SWIFT_CONTROLLERS_FLAGS"]) +myenv.MergeFlags(env["SWIFTOOLS_FLAGS"]) myenv.MergeFlags(env["SWIFTEN_FLAGS"]) myenv.MergeFlags(env["CPPUNIT_FLAGS"]) myenv.MergeFlags(env["LIBIDN_FLAGS"]) myenv.MergeFlags(env["BOOST_FLAGS"]) myenv.MergeFlags(env["SQLITE_FLAGS"]) myenv.MergeFlags(env["ZLIB_FLAGS"]) myenv.MergeFlags(env["OPENSSL_FLAGS"]) myenv.MergeFlags(env.get("LIBXML_FLAGS", "")) myenv.MergeFlags(env.get("EXPAT_FLAGS", "")) myenv.Tool("qt4", toolpath = ["#/BuildTools/SCons/Tools"]) myenv.Tool("nsis", toolpath = ["#/BuildTools/SCons/Tools"]) myenv.EnableQt4Modules(['QtCore', 'QtGui', 'QtWebKit'], debug = False) myenv.Append(CPPPATH = ["/usr/include/phonon"]) myenv.Append(CPPPATH = ["."]) if env["PLATFORM"] == "win32" : #myenv["LINKFLAGS"] = ["/SUBSYSTEM:CONSOLE"] myenv.Append(LINKFLAGS = ["/SUBSYSTEM:WINDOWS"]) myenv.Append(LIBS = "qtmain") myenv.Command("DefaultTheme.qrc", "../resources/themes/Default", Action(generateDefaultTheme, cmdstr = "$GENCOMSTR")) sources = [ "main.cpp", "QtAboutWidget.cpp", "QtAddContactDialog.cpp", "QtChatWindow.cpp", "QtChatWindowFactory.cpp", "QtIdleDetector.cpp", "QtJoinMUCDialog.cpp", "QtLoginWindow.cpp", "QtLoginWindowFactory.cpp", "QtMainWindow.cpp", "QtMainWindowFactory.cpp", "QtSettingsProvider.cpp", "QtStatusWidget.cpp", "QtSwift.cpp", "QtChatView.cpp", "QtChatTabs.cpp", "QtSoundPlayer.cpp", "QtSystemTray.cpp", "QtTabbable.cpp", "QtTextEdit.cpp", "ChatSnippet.cpp", "MessageSnippet.cpp", "SystemMessageSnippet.cpp", |
Swift