improved chatterino native

This commit is contained in:
fourtf 2018-05-28 18:25:19 +02:00
parent cded61d28d
commit 0f8375a2f3
11 changed files with 378 additions and 142 deletions

View file

@ -11,10 +11,21 @@ const ignoredPages = {
const appName = "com.chatterino.chatterino"; const appName = "com.chatterino.chatterino";
let port = null; 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() { function connectPort() {
port = chrome.runtime.connectNative("com.chatterino.chatterino"); port = chrome.runtime.connectNative(appName);
console.log("port connected"); console.log("port connected");
port.onMessage.addListener(function (msg) { 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 activated
}
}
/// Tab listeners
chrome.tabs.onActivated.addListener((activeInfo) => { chrome.tabs.onActivated.addListener((activeInfo) => {
console.log(0)
chrome.tabs.get(activeInfo.tabId, (tab) => { chrome.tabs.get(activeInfo.tabId, (tab) => {
if (!tab) console.log(1)
return; if (!tab || !tab.url) return;
if (!tab.url) console.log(2)
return; 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) => { chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (!tab.highlighted) if (!tab.highlighted)
return; 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 /// return channel name if it should contain a chat
function matchUrl(url, tab) { function matchChannelName(url) {
if (!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; let channelName;
console.log(tab);
if (match && (channelName = match[2], !ignoredPages[channelName])) { if (match && (channelName = match[2], !ignoredPages[channelName])) {
console.log("channelName " + channelName); return channelName;
console.log("winId " + tab.windowId);
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
});
} }
return undefined;
}
// attach or detach from tab
function onTabSelected(url, tab) {
let channelName = matchChannelName(url);
if (channelName) {
chrome.windows.get(tab.windowId, {}, (window) => {
// attach to window
tryAttach(tab.windowId, {
name: channelName,
yOffset: window.height - tab.height,
});
}); });
} else { } else {
// 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(); let port = getPort();
if (port) {
port.postMessage(data);
}
}
// detach chatterino from a chrome window
function tryDetach(windowId) {
let port = getPort();
if (port) { if (port) {
port.postMessage({ port.postMessage({
action: "detach", action: "detach",
winId: "" + tab.windowId winId: "" + windowId
}) })
} }
}
} }

57
browser_ext/inject.js Normal file
View file

@ -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");
})()

View file

@ -3,7 +3,8 @@
"version": "1.0", "version": "1.0",
"description": "xd", "description": "xd",
"permissions": [ "permissions": [
"tabs", "nativeMessaging" "tabs",
"nativeMessaging"
], ],
"icons": { "icons": {
"256": "icon.png" "256": "icon.png"
@ -17,5 +18,16 @@
}, },
"browser_action": { "browser_action": {
"default_popup": "popup.html" "default_popup": "popup.html"
},
"content_scripts": [
{
"run_at": "document_end",
"matches": [
"https://www.twitch.tv/*"
],
"js": [
"inject.js"
]
} }
]
} }

View file

@ -20,12 +20,6 @@
#include <atomic> #include <atomic>
#ifdef Q_OS_WIN
#include <fcntl.h>
#include <io.h>
#include <stdio.h>
#endif
using namespace chatterino::singletons; using namespace chatterino::singletons;
namespace chatterino { namespace chatterino {
@ -234,52 +228,6 @@ void Application::save()
this->commands->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<uint32_t *>(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<uint32_t>(size_c[2]) << 8 |
static_cast<uint32_t>(size_c[1]) << 16 | static_cast<uint32_t>(size_c[0]) << 24;
} else {
size = size_c[0] | static_cast<uint32_t>(size_c[1]) << 8 |
static_cast<uint32_t>(size_c[2]) << 16 | static_cast<uint32_t>(size_c[3]) << 24;
}
#endif
char *b = reinterpret_cast<char *>(malloc(size + 1));
std::cin.read(b, size);
*(b + size) = '\0';
app->nativeMessaging->sendToGuiProcess(QByteArray(b, static_cast<int32_t>(size)));
free(b);
}
}
Application *getApp() Application *getApp()
{ {
assert(staticApp != nullptr); assert(staticApp != nullptr);

View file

@ -22,9 +22,8 @@ std::shared_ptr<Account> AccountModel::getItemFromRow(std::vector<QStandardItem
void AccountModel::getRowFromItem(const std::shared_ptr<Account> &item, void AccountModel::getRowFromItem(const std::shared_ptr<Account> &item,
std::vector<QStandardItem *> &row) std::vector<QStandardItem *> &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(QFont("Segoe UI", 10), Qt::FontRole);
// row[0]->setData(QColor(255, 255, 255), Qt::BackgroundRole);
} }
int AccountModel::beforeInsert(const std::shared_ptr<Account> &item, int AccountModel::beforeInsert(const std::shared_ptr<Account> &item,
@ -34,8 +33,6 @@ int AccountModel::beforeInsert(const std::shared_ptr<Account> &item,
auto row = this->createRow(); auto row = this->createRow();
util::setStringItem(row[0], item->getCategory(), false, false); 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); row[0]->setData(QFont("Segoe UI Light", 16), Qt::FontRole);
this->insertCustomRow(std::move(row), proposedIndex); this->insertCustomRow(std::move(row), proposedIndex);

View file

@ -18,7 +18,14 @@
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
#ifdef Q_OS_WIN
#include <fcntl.h>
#include <io.h>
#include <stdio.h>
#endif
int runGui(int argc, char *argv[]); int runGui(int argc, char *argv[]);
void runNativeMessagingHost();
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
@ -31,11 +38,7 @@ int main(int argc, char *argv[])
// TODO: can be any argument // TODO: can be any argument
if (args.size() > 0 && args[0].startsWith("chrome-extension://")) { if (args.size() > 0 && args[0].startsWith("chrome-extension://")) {
chatterino::Application::instantiate(argc, argv); runNativeMessagingHost();
auto app = chatterino::getApp();
app->construct();
chatterino::Application::runNativeMessagingHost();
return 0; return 0;
} }
@ -132,3 +135,45 @@ int runGui(int argc, char *argv[])
_exit(0); _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<uint32_t *>(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<uint32_t>(size_c[2]) << 8 |
static_cast<uint32_t>(size_c[1]) << 16 | static_cast<uint32_t>(size_c[0]) << 24;
} else {
size = size_c[0] | static_cast<uint32_t>(size_c[1]) << 8 |
static_cast<uint32_t>(size_c[2]) << 16 | static_cast<uint32_t>(size_c[3]) << 24;
}
#endif
std::unique_ptr<char[]> b(new char[size + 1]);
std::cin.read(b.get(), size);
*(b.get() + size) = '\0';
nm->sendToGuiProcess(QByteArray::fromRawData(b.get(), static_cast<int32_t>(size)));
}
}

View file

@ -50,6 +50,10 @@ void NativeMessagingManager::registerHost()
{ {
auto app = getApp(); auto app = getApp();
if (app->paths->isPortable()) {
return;
}
// create manifest // create manifest
QJsonDocument document; QJsonDocument document;
QJsonObject root_obj; QJsonObject root_obj;
@ -144,26 +148,33 @@ void NativeMessagingManager::ReceiverThread::handleMessage(const QJsonObject &ro
QString _type = root.value("type").toString(); QString _type = root.value("type").toString();
bool attach = root.value("attach").toBool(); bool attach = root.value("attach").toBool();
QString name = root.value("name").toString(); 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"; qDebug() << "NM type, name or winId missing";
attach = false; attach = false;
return; return;
} }
if (_type == "twitch") { if (_type == "twitch") {
util::postToThread([name, attach, winId, yOffset, app] { util::postToThread([=] {
if (!name.isEmpty()) {
app->twitch.server->watchingChannel.update( app->twitch.server->watchingChannel.update(
app->twitch.server->getOrAddChannel(name)); app->twitch.server->getOrAddChannel(name));
}
if (attach) { if (attach) {
#ifdef USEWINSDK #ifdef USEWINSDK
auto *window = auto *window = widgets::AttachedWindow::get(::GetForegroundWindow(), args);
widgets::AttachedWindow::get(::GetForegroundWindow(), winId, yOffset); if (!name.isEmpty()) {
window->setChannel(app->twitch.server->getOrAddChannel(name)); window->setChannel(app->twitch.server->getOrAddChannel(name));
window->show(); }
// window->show();
#endif #endif
} }
}); });
@ -185,7 +196,7 @@ void NativeMessagingManager::ReceiverThread::handleMessage(const QJsonObject &ro
} else { } else {
qDebug() << "NM unknown action " + action; qDebug() << "NM unknown action " + action;
} }
} } // namespace singletons
} // namespace singletons } // namespace singletons
} // namespace chatterino } // namespace chatterino

View file

@ -19,21 +19,21 @@ PathManager::PathManager(int argc, char **argv)
.replace("/", "x"); .replace("/", "x");
// Options // Options
bool portable = false; this->portable = false;
for (int i = 1; i < argc; ++i) { for (int i = 1; i < argc; ++i) {
if (strcmp(argv[i], "portable") == 0) { if (strcmp(argv[i], "portable") == 0) {
portable = true; this->portable = true;
} }
} }
if (QFileInfo::exists(QCoreApplication::applicationDirPath() + "/portable")) { if (QFileInfo::exists(QCoreApplication::applicationDirPath() + "/this->portable")) {
portable = true; this->portable = true;
} }
// Root path = %APPDATA%/Chatterino or the folder that the executable resides in // Root path = %APPDATA%/Chatterino or the folder that the executable resides in
QString rootPath; QString rootPath;
if (portable) { if (this->portable) {
rootPath.append(QCoreApplication::applicationDirPath()); rootPath.append(QCoreApplication::applicationDirPath());
} else { } else {
// Get settings path // Get settings path
@ -91,5 +91,10 @@ bool PathManager::createFolder(const QString &folderPath)
return QDir().mkpath(folderPath); return QDir().mkpath(folderPath);
} }
bool PathManager::isPortable()
{
return this->portable;
}
} // namespace singletons } // namespace singletons
} // namespace chatterino } // namespace chatterino

View file

@ -28,6 +28,10 @@ public:
QString appPathHash; QString appPathHash;
bool createFolder(const QString &folderPath); bool createFolder(const QString &folderPath);
bool isPortable();
private:
bool portable;
}; };
} // namespace singletons } // namespace singletons

View file

@ -9,6 +9,8 @@
#ifdef USEWINSDK #ifdef USEWINSDK
#include "Windows.h" #include "Windows.h"
#include "Psapi.h"
#pragma comment(lib, "Dwmapi.lib") #pragma comment(lib, "Dwmapi.lib")
#endif #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)
{ {
AttachedWindow *window = [&]() {
for (Item &item : items) { for (Item &item : items) {
if (item.hwnd == target) { if (item.hwnd == target) {
return item.window; return item.window;
} }
} }
auto *window = new AttachedWindow(target, yOffset); auto *window = new AttachedWindow(target, args.yOffset);
items.push_back(Item{target, window, winId}); 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);
}
}
if (show) {
window->show();
window->resize(size);
}
return window; return window;
} }
@ -80,26 +113,58 @@ void AttachedWindow::attachToHwnd(void *_hwnd)
HWND hwnd = HWND(this->winId()); HWND hwnd = HWND(this->winId());
HWND attached = HWND(_hwnd); HWND attached = HWND(_hwnd);
QObject::connect(timer, &QTimer::timeout, [this, hwnd, attached, timer] { 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<TCHAR[]> 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); ::SetLastError(0);
RECT xD; RECT rect;
::GetWindowRect(attached, &xD); ::GetWindowRect(attached, &rect);
if (::GetLastError() != 0) { if (::GetLastError() != 0) {
timer->stop(); timer->stop();
timer->deleteLater(); timer->deleteLater();
this->deleteLater(); this->deleteLater();
return;
} }
// set the correct z-order
HWND next = ::GetNextWindow(attached, GW_HWNDPREV); HWND next = ::GetNextWindow(attached, GW_HWNDPREV);
::SetWindowPos(hwnd, next ? next : HWND_TOPMOST, 0, 0, 0, 0, ::SetWindowPos(hwnd, next ? next : HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); 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); if (this->_height == -1) {
// ::MoveWindow(hwnd, xD.right - 360, xD.top + 82, 360 - 8, xD.bottom - xD.top - 82 - ::MoveWindow(hwnd, rect.right - this->_width - 8, rect.top + this->yOffset - 8,
// 8, this->_width, rect.bottom - rect.top - this->yOffset, false);
// 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(); timer->start();
#endif #endif

View file

@ -13,9 +13,16 @@ class AttachedWindow : public QWidget
AttachedWindow(void *target, int asdf); AttachedWindow(void *target, int asdf);
public: public:
struct GetArgs {
QString winId;
int yOffset = -1;
int width = -1;
int height = -1;
};
~AttachedWindow(); ~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); static void detach(const QString &winId);
void setChannel(ChannelPtr channel); void setChannel(ChannelPtr channel);
@ -28,6 +35,9 @@ protected:
private: private:
void *target; void *target;
int yOffset; int yOffset;
int currentYOffset;
int _width = 360;
int _height = -1;
struct { struct {
Split *split; Split *split;