From 0f8375a2f33b29953c5997c694c70bb340bcfbf3 Mon Sep 17 00:00:00 2001 From: fourtf Date: Mon, 28 May 2018 18:25:19 +0200 Subject: [PATCH] improved chatterino native --- browser_ext/background.js | 180 ++++++++++++++++------ browser_ext/inject.js | 57 +++++++ browser_ext/manifest.json | 16 +- src/application.cpp | 52 ------- src/controllers/accounts/accountmodel.cpp | 5 +- src/main.cpp | 55 ++++++- src/singletons/nativemessagingmanager.cpp | 33 ++-- src/singletons/pathmanager.cpp | 15 +- src/singletons/pathmanager.hpp | 4 + src/widgets/attachedwindow.cpp | 91 +++++++++-- src/widgets/attachedwindow.hpp | 12 +- 11 files changed, 378 insertions(+), 142 deletions(-) create mode 100644 browser_ext/inject.js diff --git a/browser_ext/background.js b/browser_ext/background.js index 5e34c3a4d..d78194bda 100644 --- a/browser_ext/background.js +++ b/browser_ext/background.js @@ -11,10 +11,21 @@ const ignoredPages = { const appName = "com.chatterino.chatterino"; let port = null; +// gets the port for communication with chatterino +function getPort() { + if (port) { + return port; + } else { + // TODO: add cooldown + connectPort(); -/// Connect to port + return port; + } +} + +/// connect to port function connectPort() { - port = chrome.runtime.connectNative("com.chatterino.chatterino"); + port = chrome.runtime.connectNative(appName); console.log("port connected"); port.onMessage.addListener(function (msg) { @@ -27,76 +38,147 @@ function connectPort() { }); } -function getPort() { - if (port) { - return port; - } else { - // TODO: add cooldown - connectPort(); - return port; - } -} - - -/// Tab listeners +// tab activated chrome.tabs.onActivated.addListener((activeInfo) => { + console.log(0) chrome.tabs.get(activeInfo.tabId, (tab) => { - if (!tab) - return; + console.log(1) + if (!tab || !tab.url) return; - if (!tab.url) - return; + console.log(2) + chrome.windows.get(tab.windowId, {}, (window) => { + if (!window.focused) return; + console.log(3) - matchUrl(tab.url, tab); + onTabSelected(tab.url, tab); + }); }); }); +// url changed chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { if (!tab.highlighted) return; - matchUrl(changeInfo.url, tab); + onTabSelected(changeInfo.url, tab); +}); + +// tab detached +chrome.tabs.onDetached.addListener((tabId, detachInfo) => { + tryDetach(detachInfo.oldWindowId); +}); + +// tab closed +chrome.windows.onRemoved.addListener((windowId) => { + tryDetach(windowId); +}); + +// window selected +chrome.windows.onFocusChanged.addListener((windowId) => { + chrome.tabs.query({windowId: windowId, highlighted: true}, (tabs) => { + if (tabs.length >= 1) { + let tab = tabs[0]; + + onTabSelected(tab.url, tab); + } + }); }); -/// Misc -function matchUrl(url, tab) { +/// return channel name if it should contain a chat +function matchChannelName(url) { if (!url) - return; + return undefined; - const match = url.match(/^https?:\/\/(www\.)?twitch.tv\/([a-zA-Z0-9]+)\/?$/); + const match = url.match(/^https?:\/\/(www\.)?twitch.tv\/([a-zA-Z0-9_]+)\/?$/); let channelName; - - console.log(tab); - if (match && (channelName = match[2], !ignoredPages[channelName])) { - console.log("channelName " + channelName); - console.log("winId " + tab.windowId); + return channelName; + } + return undefined; +} + +// attach or detach from tab +function onTabSelected(url, tab) { + let channelName = matchChannelName(url); + + if (channelName) { chrome.windows.get(tab.windowId, {}, (window) => { - let yOffset = window.height - tab.height; - - let port = getPort(); - if (port) { - port.postMessage({ - action: "select", - attach: true, - type: "twitch", - name: channelName, - winId: "" + tab.windowId, - yOffset: yOffset - }); - } + // attach to window + tryAttach(tab.windowId, { + name: channelName, + yOffset: window.height - tab.height, + }); }); } else { - let port = getPort(); - if (port) { - port.postMessage({ - action: "detach", - winId: "" + tab.windowId - }) - } + // detach from window + tryDetach(tab.windowId); + } +} + +// receiving messages from the inject script +function registerTheGarbage() { + chrome.runtime.onMessage.addListener((message, sender, callback) => { + // is tab highlighted + if (!sender.tab.highlighted) return; + + // is window focused + chrome.windows.get(sender.tab.windowId, {}, (window) => { + if (!window.focused) return; + + // get zoom value + chrome.tabs.getZoom(sender.tab.id, (zoom) => { + let size = { + width: message.rect.width * zoom, + height: message.rect.height * zoom, + }; + + // attach to window + tryAttach(sender.tab.windowId, { + name: matchChannelName(sender.tab.url), + size: size, + }) + }); + }); + }); +} + +function registerLoop() { + // loop until the runtime objects exists because I can't be arsed to figure out the proper way to do this + if (chrome.runtime === undefined) { + setTimeout(registerLoop(), 100); + return; + } + + registerTheGarbage(); +} +registerLoop(); + +// attach chatterino to a chrome window +function tryAttach(windowId, data) { + data.action = "select"; + data.attach = true; + data.type = "twitch"; + data.winId = "" + windowId; + + let port = getPort(); + + if (port) { + port.postMessage(data); + } +} + +// detach chatterino from a chrome window +function tryDetach(windowId) { + let port = getPort(); + + if (port) { + port.postMessage({ + action: "detach", + winId: "" + windowId + }) } } diff --git a/browser_ext/inject.js b/browser_ext/inject.js new file mode 100644 index 000000000..51e87b466 --- /dev/null +++ b/browser_ext/inject.js @@ -0,0 +1,57 @@ +(() => { + let lastRect = {}; + + function log(str) { + console.log("Chatterino Native: " + str); + } + + function findChatDiv() { + return document.getElementsByClassName("right-column")[0]; + } + + function queryChatRect() { + let element = findChatDiv(); + + if (element === undefined) { + log("failed to find chat div"); + return; + } + + // element.firstChild.style.opacity = 0; + + let rect = element.getBoundingClientRect(); + // if ( + // lastRect.left == rect.left && + // lastRect.right == rect.right && + // lastRect.top == rect.top && + // lastRect.bottom == rect.bottom + // ) { + // // log("skipped sending message"); + + // return; + // } + lastRect = rect; + + let data = { + rect: rect, + }; + + chrome.runtime.sendMessage(data, (response) => { + // log("received message response"); + // console.log(response) + }); + } + + function queryCharRectLoop() { + let t1 = performance.now(); + queryChatRect(); + let t2 = performance.now(); + console.log("queryCharRect " + (t2 - t1) + "ms"); + setTimeout(queryCharRectLoop, 500); + } + + queryCharRectLoop(); + window.addEventListener("resize", queryChatRect); + + log("initialized"); +})() diff --git a/browser_ext/manifest.json b/browser_ext/manifest.json index 74d9fae08..a3829d3c0 100644 --- a/browser_ext/manifest.json +++ b/browser_ext/manifest.json @@ -3,7 +3,8 @@ "version": "1.0", "description": "xd", "permissions": [ - "tabs", "nativeMessaging" + "tabs", + "nativeMessaging" ], "icons": { "256": "icon.png" @@ -17,5 +18,16 @@ }, "browser_action": { "default_popup": "popup.html" - } + }, + "content_scripts": [ + { + "run_at": "document_end", + "matches": [ + "https://www.twitch.tv/*" + ], + "js": [ + "inject.js" + ] + } + ] } diff --git a/src/application.cpp b/src/application.cpp index 4d3c89d42..90b95741e 100644 --- a/src/application.cpp +++ b/src/application.cpp @@ -20,12 +20,6 @@ #include -#ifdef Q_OS_WIN -#include -#include -#include -#endif - using namespace chatterino::singletons; namespace chatterino { @@ -234,52 +228,6 @@ void Application::save() this->commands->save(); } -void Application::runNativeMessagingHost() -{ - auto app = getApp(); - - app->nativeMessaging = new singletons::NativeMessagingManager; - -#ifdef Q_OS_WIN - _setmode(_fileno(stdin), _O_BINARY); - _setmode(_fileno(stdout), _O_BINARY); -#endif - -#if 0 - bool bigEndian = isBigEndian(); -#endif - - while (true) { - char size_c[4]; - std::cin.read(size_c, 4); - - if (std::cin.eof()) { - break; - } - - uint32_t size = *reinterpret_cast(size_c); -#if 0 - // To avoid breaking strict-aliasing rules and potentially inducing undefined behaviour, the following code can be run instead - uint32_t size = 0; - if (bigEndian) { - size = size_c[3] | static_cast(size_c[2]) << 8 | - static_cast(size_c[1]) << 16 | static_cast(size_c[0]) << 24; - } else { - size = size_c[0] | static_cast(size_c[1]) << 8 | - static_cast(size_c[2]) << 16 | static_cast(size_c[3]) << 24; - } -#endif - - char *b = reinterpret_cast(malloc(size + 1)); - std::cin.read(b, size); - *(b + size) = '\0'; - - app->nativeMessaging->sendToGuiProcess(QByteArray(b, static_cast(size))); - - free(b); - } -} - Application *getApp() { assert(staticApp != nullptr); diff --git a/src/controllers/accounts/accountmodel.cpp b/src/controllers/accounts/accountmodel.cpp index 635474479..f84a60e92 100644 --- a/src/controllers/accounts/accountmodel.cpp +++ b/src/controllers/accounts/accountmodel.cpp @@ -22,9 +22,8 @@ std::shared_ptr AccountModel::getItemFromRow(std::vector &item, std::vector &row) { - row[0]->setData(item->toString(), Qt::DisplayRole); + util::setStringItem(row[0], item->toString(), false); row[0]->setData(QFont("Segoe UI", 10), Qt::FontRole); - // row[0]->setData(QColor(255, 255, 255), Qt::BackgroundRole); } int AccountModel::beforeInsert(const std::shared_ptr &item, @@ -34,8 +33,6 @@ int AccountModel::beforeInsert(const std::shared_ptr &item, auto row = this->createRow(); util::setStringItem(row[0], item->getCategory(), false, false); - // row[0]->setData(QColor(142, 36, 170), Qt::ForegroundRole); - // row[0]->setData(QColor(255, 255, 255), Qt::BackgroundRole); row[0]->setData(QFont("Segoe UI Light", 16), Qt::FontRole); this->insertCustomRow(std::move(row), proposedIndex); diff --git a/src/main.cpp b/src/main.cpp index 5c3607016..ab8bfc622 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -18,7 +18,14 @@ #include #include +#ifdef Q_OS_WIN +#include +#include +#include +#endif + int runGui(int argc, char *argv[]); +void runNativeMessagingHost(); int main(int argc, char *argv[]) { @@ -31,11 +38,7 @@ int main(int argc, char *argv[]) // TODO: can be any argument if (args.size() > 0 && args[0].startsWith("chrome-extension://")) { - chatterino::Application::instantiate(argc, argv); - auto app = chatterino::getApp(); - app->construct(); - - chatterino::Application::runNativeMessagingHost(); + runNativeMessagingHost(); return 0; } @@ -132,3 +135,45 @@ int runGui(int argc, char *argv[]) _exit(0); } + +void runNativeMessagingHost() +{ + auto *nm = new chatterino::singletons::NativeMessagingManager; + +#ifdef Q_OS_WIN + _setmode(_fileno(stdin), _O_BINARY); + _setmode(_fileno(stdout), _O_BINARY); +#endif + +#if 0 + bool bigEndian = isBigEndian(); +#endif + + while (true) { + char size_c[4]; + std::cin.read(size_c, 4); + + if (std::cin.eof()) { + break; + } + + uint32_t size = *reinterpret_cast(size_c); +#if 0 + // To avoid breaking strict-aliasing rules and potentially inducing undefined behaviour, the following code can be run instead + uint32_t size = 0; + if (bigEndian) { + size = size_c[3] | static_cast(size_c[2]) << 8 | + static_cast(size_c[1]) << 16 | static_cast(size_c[0]) << 24; + } else { + size = size_c[0] | static_cast(size_c[1]) << 8 | + static_cast(size_c[2]) << 16 | static_cast(size_c[3]) << 24; + } +#endif + + std::unique_ptr b(new char[size + 1]); + std::cin.read(b.get(), size); + *(b.get() + size) = '\0'; + + nm->sendToGuiProcess(QByteArray::fromRawData(b.get(), static_cast(size))); + } +} diff --git a/src/singletons/nativemessagingmanager.cpp b/src/singletons/nativemessagingmanager.cpp index 6e371d233..c415c0098 100644 --- a/src/singletons/nativemessagingmanager.cpp +++ b/src/singletons/nativemessagingmanager.cpp @@ -50,6 +50,10 @@ void NativeMessagingManager::registerHost() { auto app = getApp(); + if (app->paths->isPortable()) { + return; + } + // create manifest QJsonDocument document; QJsonObject root_obj; @@ -144,26 +148,33 @@ void NativeMessagingManager::ReceiverThread::handleMessage(const QJsonObject &ro QString _type = root.value("type").toString(); bool attach = root.value("attach").toBool(); QString name = root.value("name").toString(); - QString winId = root.value("winId").toString(); - int yOffset = root.value("yOffset").toInt(-1); - if (_type.isNull() || name.isNull() || winId.isNull()) { + widgets::AttachedWindow::GetArgs args; + args.winId = root.value("winId").toString(); + args.yOffset = root.value("yOffset").toInt(-1); + args.width = root.value("size").toObject().value("width").toInt(-1); + args.height = root.value("size").toObject().value("height").toInt(-1); + + if (_type.isNull() || args.winId.isNull()) { qDebug() << "NM type, name or winId missing"; attach = false; return; } if (_type == "twitch") { - util::postToThread([name, attach, winId, yOffset, app] { - app->twitch.server->watchingChannel.update( - app->twitch.server->getOrAddChannel(name)); + util::postToThread([=] { + if (!name.isEmpty()) { + app->twitch.server->watchingChannel.update( + app->twitch.server->getOrAddChannel(name)); + } if (attach) { #ifdef USEWINSDK - auto *window = - widgets::AttachedWindow::get(::GetForegroundWindow(), winId, yOffset); - window->setChannel(app->twitch.server->getOrAddChannel(name)); - window->show(); + auto *window = widgets::AttachedWindow::get(::GetForegroundWindow(), args); + if (!name.isEmpty()) { + window->setChannel(app->twitch.server->getOrAddChannel(name)); + } +// window->show(); #endif } }); @@ -185,7 +196,7 @@ void NativeMessagingManager::ReceiverThread::handleMessage(const QJsonObject &ro } else { qDebug() << "NM unknown action " + action; } -} +} // namespace singletons } // namespace singletons } // namespace chatterino diff --git a/src/singletons/pathmanager.cpp b/src/singletons/pathmanager.cpp index 07f5a43b8..fb87e6a43 100644 --- a/src/singletons/pathmanager.cpp +++ b/src/singletons/pathmanager.cpp @@ -19,21 +19,21 @@ PathManager::PathManager(int argc, char **argv) .replace("/", "x"); // Options - bool portable = false; + this->portable = false; for (int i = 1; i < argc; ++i) { if (strcmp(argv[i], "portable") == 0) { - portable = true; + this->portable = true; } } - if (QFileInfo::exists(QCoreApplication::applicationDirPath() + "/portable")) { - portable = true; + if (QFileInfo::exists(QCoreApplication::applicationDirPath() + "/this->portable")) { + this->portable = true; } // Root path = %APPDATA%/Chatterino or the folder that the executable resides in QString rootPath; - if (portable) { + if (this->portable) { rootPath.append(QCoreApplication::applicationDirPath()); } else { // Get settings path @@ -91,5 +91,10 @@ bool PathManager::createFolder(const QString &folderPath) return QDir().mkpath(folderPath); } +bool PathManager::isPortable() +{ + return this->portable; +} + } // namespace singletons } // namespace chatterino diff --git a/src/singletons/pathmanager.hpp b/src/singletons/pathmanager.hpp index 1c4dd888c..5d377587a 100644 --- a/src/singletons/pathmanager.hpp +++ b/src/singletons/pathmanager.hpp @@ -28,6 +28,10 @@ public: QString appPathHash; bool createFolder(const QString &folderPath); + bool isPortable(); + +private: + bool portable; }; } // namespace singletons diff --git a/src/widgets/attachedwindow.cpp b/src/widgets/attachedwindow.cpp index 9de85b9fd..1b0b84b07 100644 --- a/src/widgets/attachedwindow.cpp +++ b/src/widgets/attachedwindow.cpp @@ -9,6 +9,8 @@ #ifdef USEWINSDK #include "Windows.h" + +#include "Psapi.h" #pragma comment(lib, "Dwmapi.lib") #endif @@ -40,16 +42,47 @@ AttachedWindow::~AttachedWindow() } } -AttachedWindow *AttachedWindow::get(void *target, const QString &winId, int yOffset) +AttachedWindow *AttachedWindow::get(void *target, const GetArgs &args) { - for (Item &item : items) { - if (item.hwnd == target) { - return item.window; + AttachedWindow *window = [&]() { + for (Item &item : items) { + if (item.hwnd == target) { + return item.window; + } + } + + auto *window = new AttachedWindow(target, args.yOffset); + items.push_back(Item{target, window, args.winId}); + return window; + }(); + + bool show = true; + QSize size = window->size(); + + if (args.height != -1) { + if (args.height == 0) { + window->hide(); + show = false; + } else { + window->_height = args.height; + size.setHeight(args.height); + } + } + if (args.width != -1) { + if (args.width == 0) { + window->hide(); + show = false; + } else { + window->_width = args.width; + size.setWidth(args.width); } } - auto *window = new AttachedWindow(target, yOffset); - items.push_back(Item{target, window, winId}); + if (show) { + window->show(); + window->resize(size); + } + return window; } @@ -80,26 +113,58 @@ void AttachedWindow::attachToHwnd(void *_hwnd) HWND hwnd = HWND(this->winId()); HWND attached = HWND(_hwnd); + QObject::connect(timer, &QTimer::timeout, [this, hwnd, attached, timer] { + + // check process id + DWORD processId; + ::GetWindowThreadProcessId(attached, &processId); + + HANDLE process = + ::OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, processId); + + std::unique_ptr filename(new TCHAR[512]); + DWORD filenameLength = ::GetModuleFileNameEx(process, nullptr, filename.get(), 512); + QString qfilename = QString::fromWCharArray(filename.get(), filenameLength); + + if (!qfilename.endsWith("chrome.exe")) { + qDebug() << "NM Illegal callee" << qfilename; + timer->stop(); + timer->deleteLater(); + this->deleteLater(); + return; + } + + // We get the window rect first so we can close this window when it returns an error. + // If we query the process first and check the filename then it will return and empty string + // that doens't match. ::SetLastError(0); - RECT xD; - ::GetWindowRect(attached, &xD); + RECT rect; + ::GetWindowRect(attached, &rect); if (::GetLastError() != 0) { timer->stop(); timer->deleteLater(); this->deleteLater(); + return; } + // set the correct z-order HWND next = ::GetNextWindow(attached, GW_HWNDPREV); ::SetWindowPos(hwnd, next ? next : HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); - ::MoveWindow(hwnd, xD.right - 360, xD.top + this->yOffset - 8, 360 - 8, - xD.bottom - xD.top - this->yOffset, false); - // ::MoveWindow(hwnd, xD.right - 360, xD.top + 82, 360 - 8, xD.bottom - xD.top - 82 - - // 8, - // false); + + if (this->_height == -1) { + ::MoveWindow(hwnd, rect.right - this->_width - 8, rect.top + this->yOffset - 8, + this->_width, rect.bottom - rect.top - this->yOffset, false); + } else { + ::MoveWindow(hwnd, rect.right - this->_width - 8, rect.bottom - this->_height - 8, + this->_width, this->_height, false); + } + + // ::MoveWindow(hwnd, rect.right - 360, rect.top + 82, 360 - 8, rect.bottom - + // rect.top - 82 - 8, false); }); timer->start(); #endif diff --git a/src/widgets/attachedwindow.hpp b/src/widgets/attachedwindow.hpp index a161f4164..8c8dd6bf8 100644 --- a/src/widgets/attachedwindow.hpp +++ b/src/widgets/attachedwindow.hpp @@ -13,9 +13,16 @@ class AttachedWindow : public QWidget AttachedWindow(void *target, int asdf); public: + struct GetArgs { + QString winId; + int yOffset = -1; + int width = -1; + int height = -1; + }; + ~AttachedWindow(); - static AttachedWindow *get(void *target, const QString &winId, int yOffset); + static AttachedWindow *get(void *target, const GetArgs &args); static void detach(const QString &winId); void setChannel(ChannelPtr channel); @@ -28,6 +35,9 @@ protected: private: void *target; int yOffset; + int currentYOffset; + int _width = 360; + int _height = -1; struct { Split *split;