715 lines
25 KiB
C++
715 lines
25 KiB
C++
#include "browserwindow.h"
|
|
|
|
#include "adblock.h"
|
|
#include "commandline.h"
|
|
#include "config.h"
|
|
#include "completionview.h"
|
|
#include "greasemonkey.h"
|
|
#include "hints.h"
|
|
#include "history.h"
|
|
#include "statusbar.h"
|
|
#include "urlutil.h"
|
|
#include "useragent.h"
|
|
|
|
#include <QApplication>
|
|
#include <QClipboard>
|
|
#include <QCursor>
|
|
#include <QEvent>
|
|
#include <QKeyEvent>
|
|
#include <QMouseEvent>
|
|
#include <QRegularExpression>
|
|
#include <QVBoxLayout>
|
|
#include <QWebEngineFullScreenRequest>
|
|
#include <QtWebEngineCore/qtwebenginecoreglobal.h>
|
|
#include <QWebEnginePage>
|
|
#include <QWebEngineProfile>
|
|
#include <QWebEngineScript>
|
|
#include <QWebEngineSettings>
|
|
#include <QWebEngineView>
|
|
#include <QWheelEvent>
|
|
#include <QWidget>
|
|
|
|
namespace {
|
|
|
|
constexpr int kScrollStep = 60;
|
|
constexpr int kCompletionLimit = 100;
|
|
// What QtWebEngine accepts.
|
|
constexpr double kZoomMin = 0.25;
|
|
constexpr double kZoomMax = 5.0;
|
|
|
|
// ":open [-w|-t] <query>" or ":o ..."; captures the prefix, flag included,
|
|
// and the query.
|
|
const QRegularExpression &openCommand()
|
|
{
|
|
static const QRegularExpression re(QStringLiteral("^(:(?:open|o) +(?:-[wt] +)?)(.*)$"));
|
|
return re;
|
|
}
|
|
|
|
// ":useragent <preset>" or ":ua ..."; captures the prefix and the name.
|
|
const QRegularExpression &userAgentCommand()
|
|
{
|
|
static const QRegularExpression re(QStringLiteral("^(:(?:useragent|ua) +)(.*)$"));
|
|
return re;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
BrowserWindow::BrowserWindow(const Session &session, const QUrl &startUrl, QWidget *parent)
|
|
: QMainWindow(parent)
|
|
, m_session(session)
|
|
, m_view(new QWebEngineView(session.profile, this))
|
|
, m_status(new StatusBar(this))
|
|
, m_commandLine(new CommandLine(this))
|
|
, m_completion(new CompletionView(this))
|
|
{
|
|
setAttribute(Qt::WA_DeleteOnClose);
|
|
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_commandLine, &QLineEdit::textEdited, this, &BrowserWindow::updateCompletion);
|
|
connect(m_commandLine, &CommandLine::completionCycled, this, &BrowserWindow::cycleCompletion);
|
|
connect(m_session.adBlocker, &AdBlocker::message, m_status, &StatusBar::showMessage);
|
|
connect(m_session.greasemonkey, &Greasemonkey::message, m_status, &StatusBar::showMessage);
|
|
|
|
qApp->installEventFilter(this);
|
|
|
|
setMode(Mode::Normal);
|
|
setZoom(config::kZoomDefault, false);
|
|
m_view->setUrl(startUrl);
|
|
focusView();
|
|
}
|
|
|
|
void BrowserWindow::setupUi()
|
|
{
|
|
setWindowTitle(QStringLiteral("att_browser"));
|
|
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);
|
|
|
|
// Nothing that comes and goes may change the page's size: every resize
|
|
// makes the page re-layout, which on YouTube means the player visibly
|
|
// jumps. So the completion floats over the bottom of the page instead of
|
|
// taking space in the layout (see placeCompletion), and the status bar and
|
|
// the command line, which replace each other, get the same fixed height.
|
|
m_completion->setParent(central);
|
|
m_status->ensurePolished();
|
|
m_commandLine->ensurePolished();
|
|
const int barHeight = qMax(m_status->sizeHint().height(), m_commandLine->sizeHint().height());
|
|
m_status->setFixedHeight(barHeight);
|
|
m_commandLine->setFixedHeight(barHeight);
|
|
|
|
m_commandLine->hide();
|
|
}
|
|
|
|
void BrowserWindow::connectView()
|
|
{
|
|
connect(m_view, &QWebEngineView::urlChanged, this, [this](const QUrl &url) {
|
|
m_status->setUrl(url);
|
|
// 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);
|
|
|
|
// Off by default in QtWebEngine: without it, element.requestFullscreen()
|
|
// (YouTube's fullscreen button, double-clicking a video) silently fails.
|
|
m_view->settings()->setAttribute(QWebEngineSettings::FullScreenSupportEnabled, true);
|
|
connect(m_view->page(), &QWebEnginePage::fullScreenRequested, this,
|
|
[this](QWebEngineFullScreenRequest request) {
|
|
request.accept();
|
|
setPageFullScreen(request.toggleOn());
|
|
});
|
|
connect(m_view, &QWebEngineView::titleChanged, this, [this](const QString &title) {
|
|
setWindowTitle(title.isEmpty() ? QStringLiteral("att_browser")
|
|
: QStringLiteral("%1 - att_browser").arg(title));
|
|
m_session.history->updateTitle(m_view->url(), 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);
|
|
return;
|
|
}
|
|
// Here rather than on urlChanged, which also fires for every redirect
|
|
// hop and in-page #anchor jump.
|
|
m_session.history->addVisit(m_view->url(), m_view->title());
|
|
});
|
|
}
|
|
|
|
void BrowserWindow::showMessage(const QString &text, bool isError)
|
|
{
|
|
m_status->showMessage(text, isError);
|
|
}
|
|
|
|
void BrowserWindow::setMode(Mode mode)
|
|
{
|
|
if (m_mode == Mode::Hint && mode != Mode::Hint)
|
|
runHintScript(hints::clearScript());
|
|
|
|
m_mode = mode;
|
|
m_pendingKeys.clear();
|
|
|
|
if (mode != Mode::Command)
|
|
m_completion->setEntries({});
|
|
|
|
switch (mode) {
|
|
case Mode::Normal:
|
|
m_status->setMode(QString());
|
|
focusView();
|
|
break;
|
|
case Mode::Insert:
|
|
m_status->setMode(QStringLiteral("insert"));
|
|
focusView();
|
|
break;
|
|
case Mode::Command:
|
|
updateCompletion();
|
|
break;
|
|
case Mode::Hint:
|
|
m_status->setMode(QStringLiteral("hint"));
|
|
break;
|
|
}
|
|
|
|
updateStatusVisibility();
|
|
}
|
|
|
|
void BrowserWindow::setPageFullScreen(bool on)
|
|
{
|
|
if (on == m_pageFullScreen)
|
|
return;
|
|
m_pageFullScreen = on;
|
|
|
|
if (on) {
|
|
m_windowStateBeforeFullScreen = windowState();
|
|
showFullScreen();
|
|
} else {
|
|
setWindowState(m_windowStateBeforeFullScreen);
|
|
}
|
|
updateStatusVisibility();
|
|
}
|
|
|
|
void BrowserWindow::updateStatusVisibility()
|
|
{
|
|
// The command line replaces the status bar, and a fullscreen video gets the
|
|
// whole screen. Decided in one place so the bar is never shown just to be
|
|
// hidden again: toggling it while fullscreen knocks the window out of
|
|
// fullscreen.
|
|
m_status->setVisible(m_mode != Mode::Command && !m_pageFullScreen);
|
|
}
|
|
|
|
bool BrowserWindow::eventFilter(QObject *watched, QEvent *event)
|
|
{
|
|
if (watched == m_view && event->type() == QEvent::Resize && m_completion->isVisible())
|
|
placeCompletion();
|
|
|
|
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)) {
|
|
// Escape is swallowed here, so the page would never see it to leave
|
|
// fullscreen itself. In insert/hint mode it still just returns to normal
|
|
// mode first, as everywhere else.
|
|
if (m_mode == Mode::Normal && m_pageFullScreen)
|
|
m_view->triggerPageAction(QWebEnginePage::ExitFullScreen);
|
|
else
|
|
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;
|
|
}
|
|
if (sequence == QLatin1String("gi")) {
|
|
focusInput();
|
|
return true;
|
|
}
|
|
if (sequence == QLatin1String("go") || sequence == QLatin1String("gO")) {
|
|
// Edit the current URL, to open it here or in a new window.
|
|
openCommandLine((sequence == QLatin1String("go") ? QStringLiteral(":open ")
|
|
: QStringLiteral(":open -w "))
|
|
+ m_view->url().toString());
|
|
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")) {
|
|
openCommandLine(QStringLiteral(":open "));
|
|
return true;
|
|
}
|
|
if (text == QLatin1String("O")) {
|
|
openCommandLine(QStringLiteral(":open -w "));
|
|
return true;
|
|
}
|
|
if (text == QLatin1String("d")) {
|
|
close();
|
|
return true;
|
|
}
|
|
if (text == QLatin1String(":")) {
|
|
openCommandLine(QStringLiteral(":"));
|
|
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")) {
|
|
scrollByCssPx(0, config::kScrollStepPx);
|
|
return true;
|
|
}
|
|
if (text == QLatin1String("k")) {
|
|
scrollByCssPx(0, -config::kScrollStepPx);
|
|
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("+")) {
|
|
zoomStep(1);
|
|
return true;
|
|
}
|
|
if (text == QLatin1String("-")) {
|
|
zoomStep(-1);
|
|
return true;
|
|
}
|
|
if (text == QLatin1String("=")) {
|
|
setZoom(config::kZoomDefault);
|
|
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::focusInput()
|
|
{
|
|
m_view->page()->runJavaScript(hints::focusInputScript(), QWebEngineScript::ApplicationWorld,
|
|
[this](const QVariant &focused) {
|
|
// Ignore a result that arrives after the
|
|
// user already moved on, e.g. to ":".
|
|
if (m_mode != Mode::Normal)
|
|
return;
|
|
if (focused.toBool())
|
|
setMode(Mode::Insert);
|
|
else
|
|
m_status->showMessage(QStringLiteral("No input field found"), true);
|
|
});
|
|
}
|
|
|
|
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")) {
|
|
// qutebrowser's -t (new tab) means the same as -w here: a new window.
|
|
const QString flag = args.section(QLatin1Char(' '), 0, 0);
|
|
const bool newWindow = flag == QLatin1String("-w") || flag == QLatin1String("-t");
|
|
openUrl(newWindow ? args.section(QLatin1Char(' '), 1).trimmed() : args, newWindow);
|
|
} else if (name == QLatin1String("close") || name == QLatin1String("tab-close")) {
|
|
close();
|
|
} else if (name == QLatin1String("quit") || name == QLatin1String("q")) {
|
|
// Like qutebrowser: quit the browser, not just this window.
|
|
QApplication::closeAllWindows();
|
|
} 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("version")) {
|
|
// The Chromium security patch level is what says how far behind
|
|
// Chrome's fixes this build is; "nix flake update" moves it forward.
|
|
m_status->showMessage(QStringLiteral("att_browser %1, Qt %2, Chromium %3, security patches %4")
|
|
.arg(QCoreApplication::applicationVersion(),
|
|
QString::fromLatin1(qVersion()),
|
|
QString::fromLatin1(qWebEngineChromiumVersion()),
|
|
QString::fromLatin1(qWebEngineChromiumSecurityPatchVersion())));
|
|
} else if (name == QLatin1String("zoom")) {
|
|
if (args.isEmpty()) {
|
|
setZoom(config::kZoomDefault);
|
|
} else {
|
|
// "150" or "150%", like qutebrowser.
|
|
QString level = args;
|
|
if (level.endsWith(QLatin1Char('%')))
|
|
level.chop(1);
|
|
bool ok = false;
|
|
const double percent = level.toDouble(&ok);
|
|
if (ok && percent >= kZoomMin * 100 && percent <= kZoomMax * 100)
|
|
setZoom(percent / 100);
|
|
else
|
|
m_status->showMessage(QStringLiteral("Zoom level must be %1% to %2%: %3")
|
|
.arg(kZoomMin * 100).arg(kZoomMax * 100).arg(args),
|
|
true);
|
|
}
|
|
} else if (name == QLatin1String("zoom-in")) {
|
|
zoomStep(1);
|
|
} else if (name == QLatin1String("zoom-out")) {
|
|
zoomStep(-1);
|
|
} else if (name == QLatin1String("useragent") || name == QLatin1String("ua")) {
|
|
if (args.isEmpty()) {
|
|
m_status->showMessage(QStringLiteral("User agent: %1").arg(useragent::current()));
|
|
} else if (useragent::apply(m_session.profile, args)) {
|
|
// Requests use it at once; navigator.userAgent changes with the
|
|
// next load, so say so rather than leave the page inconsistent.
|
|
m_status->showMessage(
|
|
QStringLiteral("User agent: %1 (applies from the next page load)").arg(args));
|
|
} else {
|
|
m_status->showMessage(QStringLiteral("Unknown user agent: %1").arg(args), true);
|
|
}
|
|
} else if (name == QLatin1String("history-clear")) {
|
|
if (m_session.history->clear()) {
|
|
// Also forget Chromium's own record, which colours visited links.
|
|
m_view->page()->profile()->clearAllVisitedLinks();
|
|
m_status->showMessage(QStringLiteral("History cleared"));
|
|
} else {
|
|
m_status->showMessage(QStringLiteral("Could not clear history"), true);
|
|
}
|
|
} else if (name == QLatin1String("greasemonkey-reload")) {
|
|
m_session.greasemonkey->reload();
|
|
} else if (name == QLatin1String("adblock-update")) {
|
|
m_session.adBlocker->update();
|
|
} else if (name == QLatin1String("adblock-toggle")) {
|
|
m_session.adBlocker->setEnabled(!m_session.adBlocker->isEnabled());
|
|
m_status->showMessage(m_session.adBlocker->isEnabled() ? QStringLiteral("Adblock enabled")
|
|
: QStringLiteral("Adblock disabled"));
|
|
} else {
|
|
m_status->showMessage(QStringLiteral("Unknown command: %1").arg(name), true);
|
|
}
|
|
}
|
|
|
|
void BrowserWindow::openCommandLine(const QString &text)
|
|
{
|
|
// Hide the status bar before the command line appears: showing a widget
|
|
// re-runs the layout immediately, and with both bars visible for a moment
|
|
// the page would shrink and grow back (two resizes) every time.
|
|
m_status->hide();
|
|
m_commandLine->openWith(text);
|
|
setMode(Mode::Command);
|
|
}
|
|
|
|
void BrowserWindow::updateCompletion()
|
|
{
|
|
const QString text = m_commandLine->text();
|
|
if (const QRegularExpressionMatch match = openCommand().match(text); match.hasMatch()) {
|
|
const QStringList words = match.captured(2).split(QLatin1Char(' '), Qt::SkipEmptyParts);
|
|
m_completion->setEntries(m_session.history->search(words, kCompletionLimit));
|
|
} else if (const QRegularExpressionMatch match = userAgentCommand().match(text);
|
|
match.hasMatch()) {
|
|
// Preset name in the first column, the string it sends in the second.
|
|
const QString query = match.captured(2).trimmed();
|
|
QList<History::Entry> entries;
|
|
for (const useragent::Preset &preset : useragent::presets()) {
|
|
if (preset.name.contains(query, Qt::CaseInsensitive))
|
|
entries.append({preset.name, preset.userAgent});
|
|
}
|
|
m_completion->setEntries(entries);
|
|
} else {
|
|
m_completion->setEntries({});
|
|
return;
|
|
}
|
|
placeCompletion();
|
|
}
|
|
|
|
void BrowserWindow::placeCompletion()
|
|
{
|
|
// Bottom-aligned over the page, just above the command line.
|
|
const QRect page = m_view->geometry();
|
|
const int height = qMin(m_completion->height(), page.height());
|
|
m_completion->setGeometry(page.left(), page.bottom() + 1 - height, page.width(), height);
|
|
m_completion->raise();
|
|
}
|
|
|
|
void BrowserWindow::cycleCompletion(int step)
|
|
{
|
|
if (!m_completion->isVisible())
|
|
return;
|
|
const QString choice = m_completion->cycle(step);
|
|
if (choice.isEmpty())
|
|
return;
|
|
// Keep the command as the user typed it (":o " or ":open ", ":ua " or
|
|
// ":useragent ").
|
|
const QString text = m_commandLine->text();
|
|
QRegularExpressionMatch match = userAgentCommand().match(text);
|
|
if (!match.hasMatch())
|
|
match = openCommand().match(text);
|
|
const QString prefix = match.hasMatch() ? match.captured(1) : QStringLiteral(":open ");
|
|
m_commandLine->setCommandText(prefix + choice);
|
|
}
|
|
|
|
void BrowserWindow::openUrl(const QString &input, bool newWindow)
|
|
{
|
|
const QUrl url = urlutil::fromUserInput(input);
|
|
if (!url.isValid() || url.isEmpty()) {
|
|
m_status->showMessage(QStringLiteral("Invalid URL: %1").arg(input), true);
|
|
return;
|
|
}
|
|
if (newWindow) {
|
|
(new BrowserWindow(m_session, url))->show();
|
|
return;
|
|
}
|
|
m_view->setUrl(url);
|
|
focusView();
|
|
}
|
|
|
|
void BrowserWindow::scrollBy(int dx, int dy)
|
|
{
|
|
// A pixel-precise wheel event, which is what a touchpad sends, rather than
|
|
// window.scrollBy(): Chromium then scrolls whatever is under the pointer,
|
|
// the way the touchpad does. window.scrollBy() only ever moves the
|
|
// document, so on sites that scroll an inner element instead (most web
|
|
// apps) it did nothing at all.
|
|
QWidget *target = m_view->focusProxy();
|
|
if (!target)
|
|
return;
|
|
|
|
QPointF pos = target->mapFromGlobal(QCursor::pos());
|
|
if (!target->rect().contains(pos.toPoint()))
|
|
pos = QRectF(target->rect()).center();
|
|
|
|
// Qt's wheel deltas point the way the content moves, the opposite of the
|
|
// scroll direction.
|
|
const QPoint pixels(-dx, -dy);
|
|
QWheelEvent event(pos, target->mapToGlobal(pos), pixels, pixels * 2, Qt::NoButton,
|
|
Qt::NoModifier, Qt::NoScrollPhase, false);
|
|
QCoreApplication::sendEvent(target, &event);
|
|
}
|
|
|
|
void BrowserWindow::scrollByCssPx(int dx, int dy)
|
|
{
|
|
// Wheel deltas are screen pixels, which the page zoom shrinks (at 125% a
|
|
// 25px wheel moves 20 CSS px). qutebrowser's scroll-px counts CSS pixels.
|
|
const double zoom = m_view->zoomFactor();
|
|
scrollBy(qRound(dx * zoom), qRound(dy * zoom));
|
|
}
|
|
|
|
void BrowserWindow::scrollPages(double factor)
|
|
{
|
|
scrollBy(0, qRound(m_view->height() * factor));
|
|
}
|
|
|
|
void BrowserWindow::scrollToEnd(bool bottom)
|
|
{
|
|
// Far more than any page is tall; Chromium stops at the end.
|
|
constexpr int kFar = 10'000'000;
|
|
scrollBy(0, bottom ? kFar : -kFar);
|
|
}
|
|
|
|
void BrowserWindow::setZoom(double factor, bool announce)
|
|
{
|
|
const double zoom = qBound(kZoomMin, factor, kZoomMax);
|
|
m_view->setZoomFactor(zoom);
|
|
if (announce)
|
|
m_status->showMessage(QStringLiteral("Zoom level: %1%").arg(qRound(zoom * 100)));
|
|
}
|
|
|
|
void BrowserWindow::zoomStep(int direction)
|
|
{
|
|
// The next level strictly beyond the current zoom, so a zoom set with
|
|
// ":zoom 130" still steps to 150 / 125. The epsilon absorbs 0.33 vs 1/3.
|
|
constexpr double kEpsilon = 0.001;
|
|
const double current = m_view->zoomFactor();
|
|
const auto &levels = config::kZoomLevels;
|
|
if (direction > 0) {
|
|
for (double level : levels) {
|
|
if (level > current + kEpsilon) {
|
|
setZoom(level);
|
|
return;
|
|
}
|
|
}
|
|
} else {
|
|
for (auto it = levels.rbegin(); it != levels.rend(); ++it) {
|
|
if (*it < current - kEpsilon) {
|
|
setZoom(*it);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
m_status->showMessage(QStringLiteral("Zoom level: %1% (limit)").arg(qRound(current * 100)));
|
|
}
|
|
|
|
void BrowserWindow::focusView()
|
|
{
|
|
m_view->setFocus(Qt::OtherFocusReason);
|
|
}
|