add base: browser window, hints, history, cookies, adblock

This commit is contained in:
2026-10-15 23:03:56 +02:00
commit a841178cf7
26 changed files with 2711 additions and 0 deletions
+410
View File
@@ -0,0 +1,410 @@
#include "browserwindow.h"
#include "adblock.h"
#include "commandline.h"
#include "hints.h"
#include "statusbar.h"
#include "urlutil.h"
#include <QApplication>
#include <QClipboard>
#include <QEvent>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QVBoxLayout>
#include <QWebEnginePage>
#include <QWebEngineScript>
#include <QWebEngineView>
#include <QWidget>
namespace {
constexpr int kScrollStep = 60;
constexpr double kZoomStep = 0.1;
} // namespace
BrowserWindow::BrowserWindow(QWebEngineProfile *profile, AdBlocker *adBlocker,
const QUrl &startUrl, QWidget *parent)
: QMainWindow(parent)
, m_adBlocker(adBlocker)
, m_view(new QWebEngineView(profile, this))
, m_status(new StatusBar(this))
, m_commandLine(new CommandLine(this))
{
setupUi();
connectView();
connect(m_commandLine, &CommandLine::commandEntered, this, [this](const QString &c) {
setMode(Mode::Normal);
runCommand(c);
});
connect(m_commandLine, &CommandLine::cancelled, this, [this] {
setMode(Mode::Normal);
});
connect(m_adBlocker, &AdBlocker::message, m_status, &StatusBar::showMessage);
qApp->installEventFilter(this);
setMode(Mode::Normal);
m_view->setUrl(startUrl);
focusView();
}
void BrowserWindow::setupUi()
{
setWindowTitle(QStringLiteral("att"));
resize(1280, 800);
auto *central = new QWidget(this);
auto *layout = new QVBoxLayout(central);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
layout->addWidget(m_view, 1);
layout->addWidget(m_status);
layout->addWidget(m_commandLine);
setCentralWidget(central);
m_commandLine->hide();
}
void BrowserWindow::connectView()
{
connect(m_view, &QWebEngineView::urlChanged, this, [this](const QUrl &url) {
m_status->setUrl(url.toDisplayString());
// The hints belong to the page that is going away.
if (m_mode == Mode::Hint)
setMode(Mode::Normal);
});
connect(m_view, &QWebEngineView::loadProgress, m_status, &StatusBar::setProgress);
connect(m_view, &QWebEngineView::titleChanged, this, [this](const QString &title) {
setWindowTitle(title.isEmpty() ? QStringLiteral("att")
: QStringLiteral("%1 - att").arg(title));
});
connect(m_view, &QWebEngineView::loadFinished, this, [this](bool ok) {
m_status->setProgress(100);
if (!ok)
m_status->showMessage(QStringLiteral("Failed to load page"), true);
});
}
void BrowserWindow::setMode(Mode mode)
{
if (m_mode == Mode::Hint && mode != Mode::Hint)
runHintScript(hints::clearScript());
m_mode = mode;
m_pendingKeys.clear();
switch (mode) {
case Mode::Normal:
m_status->setMode(QString());
m_status->show();
focusView();
break;
case Mode::Insert:
m_status->setMode(QStringLiteral("insert"));
m_status->show();
focusView();
break;
case Mode::Command:
m_status->hide();
break;
case Mode::Hint:
m_status->setMode(QStringLiteral("hint"));
m_status->show();
break;
}
}
bool BrowserWindow::eventFilter(QObject *watched, QEvent *event)
{
if (event->type() != QEvent::KeyPress || !isAncestorOf(qobject_cast<QWidget *>(watched)))
return QMainWindow::eventFilter(watched, event);
auto *key = static_cast<QKeyEvent *>(event);
if (m_mode == Mode::Command)
return QMainWindow::eventFilter(watched, event);
if (key->key() == Qt::Key_Escape
|| (key->key() == Qt::Key_BracketLeft && key->modifiers() == Qt::ControlModifier)) {
setMode(Mode::Normal);
return true;
}
if (m_mode == Mode::Insert)
return QMainWindow::eventFilter(watched, event);
if (m_mode == Mode::Hint) {
handleHintModeKey(key);
return true;
}
if (handleNormalModeKey(key))
return true;
return QMainWindow::eventFilter(watched, event);
}
bool BrowserWindow::handleNormalModeKey(QKeyEvent *event)
{
const QString text = event->text();
const Qt::KeyboardModifiers mods = event->modifiers();
if (mods.testFlag(Qt::ControlModifier)) {
switch (event->key()) {
case Qt::Key_D:
scrollPages(0.5);
return true;
case Qt::Key_U:
scrollPages(-0.5);
return true;
case Qt::Key_F:
scrollPages(1.0);
return true;
case Qt::Key_B:
scrollPages(-1.0);
return true;
default:
return false;
}
}
if (mods.testFlag(Qt::AltModifier) || mods.testFlag(Qt::MetaModifier))
return false;
// Two-key sequences starting with "g".
if (!m_pendingKeys.isEmpty()) {
const QString sequence = m_pendingKeys + text;
m_pendingKeys.clear();
if (sequence == QLatin1String("gg")) {
scrollToEnd(false);
return true;
}
return true; // Swallow the unknown sequence rather than leaking it.
}
if (text == QLatin1String("g")) {
m_pendingKeys = text;
return true;
}
if (text == QLatin1String("o")) {
m_commandLine->openWith(QStringLiteral(":open "));
setMode(Mode::Command);
return true;
}
if (text == QLatin1String("O")) {
// Edit the current URL.
m_commandLine->openWith(QStringLiteral(":open ") + m_view->url().toString());
setMode(Mode::Command);
return true;
}
if (text == QLatin1String(":")) {
m_commandLine->openWith(QStringLiteral(":"));
setMode(Mode::Command);
return true;
}
if (text == QLatin1String("i")) {
setMode(Mode::Insert);
return true;
}
if (text == QLatin1String("f")) {
startHints();
return true;
}
if (text == QLatin1String("r")) {
m_view->reload();
return true;
}
if (text == QLatin1String("R")) {
m_view->triggerPageAction(QWebEnginePage::ReloadAndBypassCache);
return true;
}
if (text == QLatin1String("H")) {
m_view->back();
return true;
}
if (text == QLatin1String("L")) {
m_view->forward();
return true;
}
if (text == QLatin1String("j")) {
scrollBy(0, kScrollStep);
return true;
}
if (text == QLatin1String("k")) {
scrollBy(0, -kScrollStep);
return true;
}
if (text == QLatin1String("h")) {
scrollBy(-kScrollStep, 0);
return true;
}
if (text == QLatin1String("l")) {
scrollBy(kScrollStep, 0);
return true;
}
if (text == QLatin1String("G")) {
scrollToEnd(true);
return true;
}
if (text == QLatin1String("y")) {
const QString url = m_view->url().toString();
qApp->clipboard()->setText(url);
m_status->showMessage(QStringLiteral("Yanked %1").arg(url));
return true;
}
if (text == QLatin1String("+")) {
m_view->setZoomFactor(m_view->zoomFactor() + kZoomStep);
return true;
}
if (text == QLatin1String("-")) {
m_view->setZoomFactor(qMax(0.25, m_view->zoomFactor() - kZoomStep));
return true;
}
if (text == QLatin1String("=")) {
m_view->setZoomFactor(1.0);
return true;
}
return false;
}
void BrowserWindow::handleHintModeKey(QKeyEvent *event)
{
if (event->key() == Qt::Key_Backspace) {
runHintScript(hints::backspaceScript());
return;
}
const QString text = event->text().toLower();
if (text.size() == 1 && hints::chars().contains(text))
runHintScript(hints::keyScript(text.at(0)));
// Anything else is swallowed, so a stray key can't trigger a page shortcut.
}
void BrowserWindow::startHints()
{
setMode(Mode::Hint);
runHintScript(hints::showScript());
}
void BrowserWindow::runHintScript(const QString &script)
{
m_view->page()->runJavaScript(script, QWebEngineScript::ApplicationWorld,
[this](const QVariant &result) {
handleHintResult(result.toMap());
});
}
void BrowserWindow::handleHintResult(const QVariantMap &result)
{
// Results can arrive after the user already left hint mode, e.g. a key that
// was still in flight when Escape was pressed.
if (m_mode != Mode::Hint)
return;
const QString state = result.value(QStringLiteral("state")).toString();
if (state == QLatin1String("shown")) {
if (result.value(QStringLiteral("count")).toInt() == 0) {
setMode(Mode::Normal);
m_status->showMessage(QStringLiteral("No elements found"), true);
}
} else if (state == QLatin1String("follow")) {
const QPointF pos(result.value(QStringLiteral("x")).toDouble(),
result.value(QStringLiteral("y")).toDouble());
setMode(Mode::Normal);
clickAt(pos * m_view->zoomFactor());
if (result.value(QStringLiteral("editable")).toBool())
setMode(Mode::Insert);
} else if (state == QLatin1String("gone")) {
setMode(Mode::Normal);
m_status->showMessage(QStringLiteral("Element went away"), true);
} else if (state == QLatin1String("none") || state.isEmpty()) {
// The script failed, or the page navigated and took the hints with it.
setMode(Mode::Normal);
}
}
void BrowserWindow::clickAt(const QPointF &pos)
{
// A real mouse click rather than element.click(): plenty of sites only
// listen for mousedown/pointerdown, and it gives the page a trusted event.
QWidget *target = m_view->focusProxy();
if (!target)
return;
const QPointF global = target->mapToGlobal(pos);
QMouseEvent press(QEvent::MouseButtonPress, pos, global, Qt::LeftButton,
Qt::LeftButton, Qt::NoModifier);
QMouseEvent release(QEvent::MouseButtonRelease, pos, global, Qt::LeftButton,
Qt::NoButton, Qt::NoModifier);
QCoreApplication::sendEvent(target, &press);
QCoreApplication::sendEvent(target, &release);
}
void BrowserWindow::runCommand(const QString &command)
{
const QString name = command.section(QLatin1Char(' '), 0, 0);
const QString args = command.section(QLatin1Char(' '), 1).trimmed();
if (name == QLatin1String("open") || name == QLatin1String("o")) {
openUrl(args);
} else if (name == QLatin1String("quit") || name == QLatin1String("q")) {
close();
} else if (name == QLatin1String("reload")) {
m_view->reload();
} else if (name == QLatin1String("back")) {
m_view->back();
} else if (name == QLatin1String("forward")) {
m_view->forward();
} else if (name == QLatin1String("stop")) {
m_view->stop();
} else if (name == QLatin1String("adblock-update")) {
m_adBlocker->update();
} else if (name == QLatin1String("adblock-toggle")) {
m_adBlocker->setEnabled(!m_adBlocker->isEnabled());
m_status->showMessage(m_adBlocker->isEnabled() ? QStringLiteral("Adblock enabled")
: QStringLiteral("Adblock disabled"));
} else {
m_status->showMessage(QStringLiteral("Unknown command: %1").arg(name), true);
}
}
void BrowserWindow::openUrl(const QString &input)
{
const QUrl url = urlutil::fromUserInput(input);
if (!url.isValid() || url.isEmpty()) {
m_status->showMessage(QStringLiteral("Invalid URL: %1").arg(input), true);
return;
}
m_view->setUrl(url);
focusView();
}
void BrowserWindow::scrollBy(int dx, int dy)
{
m_view->page()->runJavaScript(
QStringLiteral("window.scrollBy(%1, %2);").arg(dx).arg(dy));
}
void BrowserWindow::scrollPages(double factor)
{
m_view->page()->runJavaScript(
QStringLiteral("window.scrollBy(0, window.innerHeight * %1);").arg(factor));
}
void BrowserWindow::scrollToEnd(bool bottom)
{
m_view->page()->runJavaScript(
bottom ? QStringLiteral("window.scrollTo(0, document.body.scrollHeight);")
: QStringLiteral("window.scrollTo(0, 0);"));
}
void BrowserWindow::focusView()
{
m_view->setFocus(Qt::OtherFocusReason);
}