add multiple windows support with shared session, add shortcuts
This commit is contained in:
@@ -25,7 +25,7 @@ The only tests are the Rust crate's (`nix develop -c sh -c 'cd adblock && cargo
|
|||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
`BrowserWindow` (`src/browserwindow.cpp`) is the core of the browser. It owns a single `QWebEngineView`, the `StatusBar` and the `CommandLine`, and it runs the mode state machine (`Normal`, `Insert`, `Command`, `Hint`).
|
`BrowserWindow` (`src/browserwindow.cpp`) is the core of the browser. Each one owns a single `QWebEngineView`, the `StatusBar` and the `CommandLine`, and it runs the mode state machine (`Normal`, `Insert`, `Command`, `Hint`).
|
||||||
|
|
||||||
- **Key handling uses an application-wide event filter** (`qApp->installEventFilter`, `BrowserWindow::eventFilter`), not `keyPressEvent`. QtWebEngine delivers keys straight to its internal render widget, so the window would never see them otherwise. Dispatch order:
|
- **Key handling uses an application-wide event filter** (`qApp->installEventFilter`, `BrowserWindow::eventFilter`), not `keyPressEvent`. QtWebEngine delivers keys straight to its internal render widget, so the window would never see them otherwise. Dispatch order:
|
||||||
1. Command mode passes keys through to the `CommandLine`.
|
1. Command mode passes keys through to the `CommandLine`.
|
||||||
@@ -53,4 +53,6 @@ The only tests are the Rust crate's (`nix develop -c sh -c 'cd adblock && cargo
|
|||||||
- **`:open` completion:** `BrowserWindow::updateCompletion` runs on the command line's `textEdited` and on entering command mode. It parses `:open`/`:o` and shows `History::search` results in `CompletionView`, which never takes focus. `CommandLine::event` intercepts Tab/Backtab, because `QWidget::event` would otherwise turn them into focus changes. Picking an entry uses `setCommandText`, which doesn't emit `textEdited`, so it doesn't re-filter.
|
- **`:open` completion:** `BrowserWindow::updateCompletion` runs on the command line's `textEdited` and on entering command mode. It parses `:open`/`:o` and shows `History::search` results in `CompletionView`, which never takes focus. `CommandLine::event` intercepts Tab/Backtab, because `QWidget::event` would otherwise turn them into focus changes. Picking an entry uses `setCommandText`, which doesn't emit `textEdited`, so it doesn't re-filter.
|
||||||
- The UI stylesheet is the `kStyleSheet` string in `src/main.cpp`. Widgets are styled by `objectName` and by dynamic properties: `StatusBar` toggles `error` and re-polishes the label to pick up the change.
|
- The UI stylesheet is the `kStyleSheet` string in `src/main.cpp`. Widgets are styled by `objectName` and by dynamic properties: `StatusBar` toggles `error` and re-polishes the label to pick up the change.
|
||||||
|
|
||||||
There are no tabs yet. `createWindow` isn't implemented, so `target=_blank` and `window.open` go nowhere. To work around this, hint mode rewrites link targets to `_self`.
|
**Windows, not tabs.** att deliberately has no tab bar, because the user's window manager does the tabbing. Map qutebrowser's tab features onto top-level windows: `d` closes the window, `O` / `:open -w` (or `-t`) opens a new one. Each `BrowserWindow` is one page, with `WA_DeleteOnClose`. All windows share a `Session` (profile, `AdBlocker`, `History`) that `main()` owns. After `app.exec()`, `main()` deletes any window still alive before the history and profile go away. `:quit` closes all windows; `:close` closes one.
|
||||||
|
|
||||||
|
`newWindowRequested` isn't handled yet, so `target=_blank` and `window.open` go nowhere. Hint mode rewrites link targets to `_self` to work around this.
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
A small, keyboard-driven QtWebEngine browser in the spirit of qutebrowser.
|
A small, keyboard-driven QtWebEngine browser in the spirit of qutebrowser.
|
||||||
|
|
||||||
This is the skeleton: one window, one page, a qutebrowser-style status line and
|
One page per window, a qutebrowser-style status line and `:` command line, `f`
|
||||||
`:` command line, `f` link hints and adblocking. No tabs and no config file yet.
|
link hints and adblocking. There is deliberately no tab bar: where qutebrowser
|
||||||
|
opens or closes a tab, att opens or closes a window, and your window manager
|
||||||
|
does the tabbing. No config file yet.
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
@@ -52,9 +54,12 @@ is hidden while a page is fullscreen.
|
|||||||
| Key | Action |
|
| Key | Action |
|
||||||
| -------------- | ---------------------------------------- |
|
| -------------- | ---------------------------------------- |
|
||||||
| `o` | open the command line prefilled `:open ` |
|
| `o` | open the command line prefilled `:open ` |
|
||||||
| `O` | same, prefilled with the current URL |
|
| `O` | same, but opens in a new window (`-w`) |
|
||||||
|
| `go` / `gO` | edit the current URL, here / new window |
|
||||||
|
| `d` | close this window |
|
||||||
| `:` | open a bare command line |
|
| `:` | open a bare command line |
|
||||||
| `i` | enter insert mode |
|
| `i` | enter insert mode |
|
||||||
|
| `gi` | focus the first text field, insert mode |
|
||||||
| `f` | hint mode: follow a link by its label |
|
| `f` | hint mode: follow a link by its label |
|
||||||
| `Escape` | back to normal mode |
|
| `Escape` | back to normal mode |
|
||||||
| `h` `j` `k` `l`| scroll |
|
| `h` `j` `k` `l`| scroll |
|
||||||
@@ -80,8 +85,7 @@ on the command line; `Enter` opens it.
|
|||||||
(`asdfghjkl`). Type a label to click it; `Backspace` undoes a letter, `Escape`
|
(`asdfghjkl`). Type a label to click it; `Backspace` undoes a letter, `Escape`
|
||||||
cancels. The click is a real mouse event sent to the render widget, so it works
|
cancels. The click is a real mouse event sent to the render widget, so it works
|
||||||
on sites that ignore `element.click()`. Following a text field drops you into
|
on sites that ignore `element.click()`. Following a text field drops you into
|
||||||
insert mode. Links that would open a new window are opened in place, since there
|
insert mode. Links that would open a new window are opened in place for now.
|
||||||
are no tabs yet.
|
|
||||||
|
|
||||||
The hint script runs in `QWebEngineScript::ApplicationWorld`, so the page's own
|
The hint script runs in `QWebEngineScript::ApplicationWorld`, so the page's own
|
||||||
JavaScript can't see or interfere with it. It doesn't descend into iframes or
|
JavaScript can't see or interfere with it. It doesn't descend into iframes or
|
||||||
@@ -115,8 +119,10 @@ on, for when a site breaks.
|
|||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
`:open <url|query>` (alias `:o`), `:quit` (alias `:q`), `:reload`, `:back`,
|
`:open [-w] <url|query>` (alias `:o`; `-w`, or qutebrowser's `-t`, opens a new
|
||||||
`:forward`, `:stop`, `:history-clear`, `:adblock-update`, `:adblock-toggle`.
|
window), `:close` (alias `:tab-close`, closes this window), `:quit` (alias `:q`,
|
||||||
|
closes all windows), `:reload`, `:back`, `:forward`, `:stop`, `:history-clear`,
|
||||||
|
`:adblock-update`, `:adblock-toggle`.
|
||||||
|
|
||||||
`:open` accepts a full URL, a bare host (`example.com` → `https://example.com`),
|
`:open` accepts a full URL, a bare host (`example.com` → `https://example.com`),
|
||||||
an existing local path, or free text, which goes to DuckDuckGo.
|
an existing local path, or free text, which goes to DuckDuckGo.
|
||||||
@@ -145,10 +151,11 @@ parent window never sees them otherwise.
|
|||||||
|
|
||||||
## Next steps
|
## Next steps
|
||||||
|
|
||||||
- Tabs (`QTabWidget` + a per-tab `QWebEngineView`), `gt`/`gT`, `d` to close.
|
- Open `target=_blank` links, `window.open` and middle-clicks in new windows
|
||||||
|
(`QWebEnginePage::newWindowRequested`).
|
||||||
- Auto insert mode: inject a `QWebEngineScript` that reports `focusin` /
|
- Auto insert mode: inject a `QWebEngineScript` that reports `focusin` /
|
||||||
`focusout` on editable elements back over `QWebChannel`.
|
`focusout` on editable elements back over `QWebChannel`.
|
||||||
- More hint modes: `F` (new tab, once there are tabs), `;y` to yank a link.
|
- More hint modes: `F` (follow in a new window), `;y` to yank a link.
|
||||||
- `/` and `?` search with `n`/`N`, using `QWebEnginePage::findText`.
|
- `/` and `?` search with `n`/`N`, using `QWebEnginePage::findText`.
|
||||||
- A config file for bindings, start page and search engine.
|
- A config file for bindings, start page and search engine.
|
||||||
- Counts before commands (`3j`) and a proper command parser with completion.
|
- Counts before commands (`3j`) and a proper command parser with completion.
|
||||||
|
|||||||
+61
-19
@@ -4,6 +4,7 @@
|
|||||||
#include "commandline.h"
|
#include "commandline.h"
|
||||||
#include "completionview.h"
|
#include "completionview.h"
|
||||||
#include "hints.h"
|
#include "hints.h"
|
||||||
|
#include "history.h"
|
||||||
#include "statusbar.h"
|
#include "statusbar.h"
|
||||||
#include "urlutil.h"
|
#include "urlutil.h"
|
||||||
|
|
||||||
@@ -28,24 +29,25 @@ constexpr int kScrollStep = 60;
|
|||||||
constexpr double kZoomStep = 0.1;
|
constexpr double kZoomStep = 0.1;
|
||||||
constexpr int kCompletionLimit = 100;
|
constexpr int kCompletionLimit = 100;
|
||||||
|
|
||||||
// ":open <query>" or ":o <query>"; captures the prefix and the query.
|
// ":open [-w|-t] <query>" or ":o ..."; captures the prefix, flag included,
|
||||||
|
// and the query.
|
||||||
const QRegularExpression &openCommand()
|
const QRegularExpression &openCommand()
|
||||||
{
|
{
|
||||||
static const QRegularExpression re(QStringLiteral("^(:(?:open|o) +)(.*)$"));
|
static const QRegularExpression re(QStringLiteral("^(:(?:open|o) +(?:-[wt] +)?)(.*)$"));
|
||||||
return re;
|
return re;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
BrowserWindow::BrowserWindow(QWebEngineProfile *profile, AdBlocker *adBlocker,
|
BrowserWindow::BrowserWindow(const Session &session, const QUrl &startUrl, QWidget *parent)
|
||||||
const QUrl &startUrl, QWidget *parent)
|
|
||||||
: QMainWindow(parent)
|
: QMainWindow(parent)
|
||||||
, m_adBlocker(adBlocker)
|
, m_session(session)
|
||||||
, m_view(new QWebEngineView(profile, this))
|
, m_view(new QWebEngineView(session.profile, this))
|
||||||
, m_status(new StatusBar(this))
|
, m_status(new StatusBar(this))
|
||||||
, m_commandLine(new CommandLine(this))
|
, m_commandLine(new CommandLine(this))
|
||||||
, m_completion(new CompletionView(this))
|
, m_completion(new CompletionView(this))
|
||||||
{
|
{
|
||||||
|
setAttribute(Qt::WA_DeleteOnClose);
|
||||||
setupUi();
|
setupUi();
|
||||||
connectView();
|
connectView();
|
||||||
|
|
||||||
@@ -58,7 +60,7 @@ BrowserWindow::BrowserWindow(QWebEngineProfile *profile, AdBlocker *adBlocker,
|
|||||||
});
|
});
|
||||||
connect(m_commandLine, &QLineEdit::textEdited, this, &BrowserWindow::updateCompletion);
|
connect(m_commandLine, &QLineEdit::textEdited, this, &BrowserWindow::updateCompletion);
|
||||||
connect(m_commandLine, &CommandLine::completionCycled, this, &BrowserWindow::cycleCompletion);
|
connect(m_commandLine, &CommandLine::completionCycled, this, &BrowserWindow::cycleCompletion);
|
||||||
connect(m_adBlocker, &AdBlocker::message, m_status, &StatusBar::showMessage);
|
connect(m_session.adBlocker, &AdBlocker::message, m_status, &StatusBar::showMessage);
|
||||||
|
|
||||||
qApp->installEventFilter(this);
|
qApp->installEventFilter(this);
|
||||||
|
|
||||||
@@ -106,7 +108,7 @@ void BrowserWindow::connectView()
|
|||||||
connect(m_view, &QWebEngineView::titleChanged, this, [this](const QString &title) {
|
connect(m_view, &QWebEngineView::titleChanged, this, [this](const QString &title) {
|
||||||
setWindowTitle(title.isEmpty() ? QStringLiteral("att")
|
setWindowTitle(title.isEmpty() ? QStringLiteral("att")
|
||||||
: QStringLiteral("%1 - att").arg(title));
|
: QStringLiteral("%1 - att").arg(title));
|
||||||
m_history.updateTitle(m_view->url(), title);
|
m_session.history->updateTitle(m_view->url(), title);
|
||||||
});
|
});
|
||||||
connect(m_view, &QWebEngineView::loadFinished, this, [this](bool ok) {
|
connect(m_view, &QWebEngineView::loadFinished, this, [this](bool ok) {
|
||||||
m_status->setProgress(100);
|
m_status->setProgress(100);
|
||||||
@@ -116,7 +118,7 @@ void BrowserWindow::connectView()
|
|||||||
}
|
}
|
||||||
// Here rather than on urlChanged, which also fires for every redirect
|
// Here rather than on urlChanged, which also fires for every redirect
|
||||||
// hop and in-page #anchor jump.
|
// hop and in-page #anchor jump.
|
||||||
m_history.addVisit(m_view->url(), m_view->title());
|
m_session.history->addVisit(m_view->url(), m_view->title());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,6 +248,18 @@ bool BrowserWindow::handleNormalModeKey(QKeyEvent *event)
|
|||||||
scrollToEnd(false);
|
scrollToEnd(false);
|
||||||
return true;
|
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.
|
||||||
|
m_commandLine->openWith((sequence == QLatin1String("go") ? QStringLiteral(":open ")
|
||||||
|
: QStringLiteral(":open -w "))
|
||||||
|
+ m_view->url().toString());
|
||||||
|
setMode(Mode::Command);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return true; // Swallow the unknown sequence rather than leaking it.
|
return true; // Swallow the unknown sequence rather than leaking it.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,11 +274,14 @@ bool BrowserWindow::handleNormalModeKey(QKeyEvent *event)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (text == QLatin1String("O")) {
|
if (text == QLatin1String("O")) {
|
||||||
// Edit the current URL.
|
m_commandLine->openWith(QStringLiteral(":open -w "));
|
||||||
m_commandLine->openWith(QStringLiteral(":open ") + m_view->url().toString());
|
|
||||||
setMode(Mode::Command);
|
setMode(Mode::Command);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (text == QLatin1String("d")) {
|
||||||
|
close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (text == QLatin1String(":")) {
|
if (text == QLatin1String(":")) {
|
||||||
m_commandLine->openWith(QStringLiteral(":"));
|
m_commandLine->openWith(QStringLiteral(":"));
|
||||||
setMode(Mode::Command);
|
setMode(Mode::Command);
|
||||||
@@ -355,6 +372,21 @@ void BrowserWindow::startHints()
|
|||||||
runHintScript(hints::showScript());
|
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)
|
void BrowserWindow::runHintScript(const QString &script)
|
||||||
{
|
{
|
||||||
m_view->page()->runJavaScript(script, QWebEngineScript::ApplicationWorld,
|
m_view->page()->runJavaScript(script, QWebEngineScript::ApplicationWorld,
|
||||||
@@ -416,9 +448,15 @@ void BrowserWindow::runCommand(const QString &command)
|
|||||||
const QString args = command.section(QLatin1Char(' '), 1).trimmed();
|
const QString args = command.section(QLatin1Char(' '), 1).trimmed();
|
||||||
|
|
||||||
if (name == QLatin1String("open") || name == QLatin1String("o")) {
|
if (name == QLatin1String("open") || name == QLatin1String("o")) {
|
||||||
openUrl(args);
|
// qutebrowser's -t (new tab) means the same as -w here: a new window.
|
||||||
} else if (name == QLatin1String("quit") || name == QLatin1String("q")) {
|
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();
|
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")) {
|
} else if (name == QLatin1String("reload")) {
|
||||||
m_view->reload();
|
m_view->reload();
|
||||||
} else if (name == QLatin1String("back")) {
|
} else if (name == QLatin1String("back")) {
|
||||||
@@ -428,7 +466,7 @@ void BrowserWindow::runCommand(const QString &command)
|
|||||||
} else if (name == QLatin1String("stop")) {
|
} else if (name == QLatin1String("stop")) {
|
||||||
m_view->stop();
|
m_view->stop();
|
||||||
} else if (name == QLatin1String("history-clear")) {
|
} else if (name == QLatin1String("history-clear")) {
|
||||||
if (m_history.clear()) {
|
if (m_session.history->clear()) {
|
||||||
// Also forget Chromium's own record, which colours visited links.
|
// Also forget Chromium's own record, which colours visited links.
|
||||||
m_view->page()->profile()->clearAllVisitedLinks();
|
m_view->page()->profile()->clearAllVisitedLinks();
|
||||||
m_status->showMessage(QStringLiteral("History cleared"));
|
m_status->showMessage(QStringLiteral("History cleared"));
|
||||||
@@ -436,10 +474,10 @@ void BrowserWindow::runCommand(const QString &command)
|
|||||||
m_status->showMessage(QStringLiteral("Could not clear history"), true);
|
m_status->showMessage(QStringLiteral("Could not clear history"), true);
|
||||||
}
|
}
|
||||||
} else if (name == QLatin1String("adblock-update")) {
|
} else if (name == QLatin1String("adblock-update")) {
|
||||||
m_adBlocker->update();
|
m_session.adBlocker->update();
|
||||||
} else if (name == QLatin1String("adblock-toggle")) {
|
} else if (name == QLatin1String("adblock-toggle")) {
|
||||||
m_adBlocker->setEnabled(!m_adBlocker->isEnabled());
|
m_session.adBlocker->setEnabled(!m_session.adBlocker->isEnabled());
|
||||||
m_status->showMessage(m_adBlocker->isEnabled() ? QStringLiteral("Adblock enabled")
|
m_status->showMessage(m_session.adBlocker->isEnabled() ? QStringLiteral("Adblock enabled")
|
||||||
: QStringLiteral("Adblock disabled"));
|
: QStringLiteral("Adblock disabled"));
|
||||||
} else {
|
} else {
|
||||||
m_status->showMessage(QStringLiteral("Unknown command: %1").arg(name), true);
|
m_status->showMessage(QStringLiteral("Unknown command: %1").arg(name), true);
|
||||||
@@ -454,7 +492,7 @@ void BrowserWindow::updateCompletion()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const QStringList words = match.captured(2).split(QLatin1Char(' '), Qt::SkipEmptyParts);
|
const QStringList words = match.captured(2).split(QLatin1Char(' '), Qt::SkipEmptyParts);
|
||||||
m_completion->setEntries(m_history.search(words, kCompletionLimit));
|
m_completion->setEntries(m_session.history->search(words, kCompletionLimit));
|
||||||
}
|
}
|
||||||
|
|
||||||
void BrowserWindow::cycleCompletion(int step)
|
void BrowserWindow::cycleCompletion(int step)
|
||||||
@@ -470,13 +508,17 @@ void BrowserWindow::cycleCompletion(int step)
|
|||||||
m_commandLine->setCommandText(prefix + url);
|
m_commandLine->setCommandText(prefix + url);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BrowserWindow::openUrl(const QString &input)
|
void BrowserWindow::openUrl(const QString &input, bool newWindow)
|
||||||
{
|
{
|
||||||
const QUrl url = urlutil::fromUserInput(input);
|
const QUrl url = urlutil::fromUserInput(input);
|
||||||
if (!url.isValid() || url.isEmpty()) {
|
if (!url.isValid() || url.isEmpty()) {
|
||||||
m_status->showMessage(QStringLiteral("Invalid URL: %1").arg(input), true);
|
m_status->showMessage(QStringLiteral("Invalid URL: %1").arg(input), true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (newWindow) {
|
||||||
|
(new BrowserWindow(m_session, url))->show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
m_view->setUrl(url);
|
m_view->setUrl(url);
|
||||||
focusView();
|
focusView();
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-7
@@ -1,7 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "history.h"
|
|
||||||
|
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
@@ -10,10 +8,21 @@
|
|||||||
class AdBlocker;
|
class AdBlocker;
|
||||||
class CommandLine;
|
class CommandLine;
|
||||||
class CompletionView;
|
class CompletionView;
|
||||||
|
class History;
|
||||||
class StatusBar;
|
class StatusBar;
|
||||||
class QWebEngineProfile;
|
class QWebEngineProfile;
|
||||||
class QWebEngineView;
|
class QWebEngineView;
|
||||||
|
|
||||||
|
// What all windows share. Owned by main(), and outlives every window.
|
||||||
|
struct Session {
|
||||||
|
QWebEngineProfile *profile;
|
||||||
|
AdBlocker *adBlocker;
|
||||||
|
History *history;
|
||||||
|
};
|
||||||
|
|
||||||
|
// One page per window: att has no tabs. Where qutebrowser opens or closes a
|
||||||
|
// tab, att opens or closes a window and leaves tabbing to the window manager.
|
||||||
|
// Windows delete themselves when closed.
|
||||||
class BrowserWindow : public QMainWindow
|
class BrowserWindow : public QMainWindow
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -26,8 +35,7 @@ public:
|
|||||||
Hint, // keys pick a link label
|
Hint, // keys pick a link label
|
||||||
};
|
};
|
||||||
|
|
||||||
BrowserWindow(QWebEngineProfile *profile, AdBlocker *adBlocker, const QUrl &startUrl,
|
BrowserWindow(const Session &session, const QUrl &startUrl, QWidget *parent = nullptr);
|
||||||
QWidget *parent = nullptr);
|
|
||||||
|
|
||||||
void setMode(Mode mode);
|
void setMode(Mode mode);
|
||||||
|
|
||||||
@@ -44,13 +52,14 @@ private:
|
|||||||
bool handleNormalModeKey(QKeyEvent *event);
|
bool handleNormalModeKey(QKeyEvent *event);
|
||||||
void handleHintModeKey(QKeyEvent *event);
|
void handleHintModeKey(QKeyEvent *event);
|
||||||
void startHints();
|
void startHints();
|
||||||
|
void focusInput();
|
||||||
void runHintScript(const QString &script);
|
void runHintScript(const QString &script);
|
||||||
void handleHintResult(const QVariantMap &result);
|
void handleHintResult(const QVariantMap &result);
|
||||||
void clickAt(const QPointF &pos);
|
void clickAt(const QPointF &pos);
|
||||||
void runCommand(const QString &command);
|
void runCommand(const QString &command);
|
||||||
void updateCompletion();
|
void updateCompletion();
|
||||||
void cycleCompletion(int step);
|
void cycleCompletion(int step);
|
||||||
void openUrl(const QString &input);
|
void openUrl(const QString &input, bool newWindow);
|
||||||
void scrollBy(int dx, int dy);
|
void scrollBy(int dx, int dy);
|
||||||
void scrollPages(double factor);
|
void scrollPages(double factor);
|
||||||
void scrollToEnd(bool bottom);
|
void scrollToEnd(bool bottom);
|
||||||
@@ -58,12 +67,11 @@ private:
|
|||||||
void setPageFullScreen(bool on);
|
void setPageFullScreen(bool on);
|
||||||
void updateStatusVisibility();
|
void updateStatusVisibility();
|
||||||
|
|
||||||
AdBlocker *m_adBlocker;
|
Session m_session;
|
||||||
QWebEngineView *m_view;
|
QWebEngineView *m_view;
|
||||||
StatusBar *m_status;
|
StatusBar *m_status;
|
||||||
CommandLine *m_commandLine;
|
CommandLine *m_commandLine;
|
||||||
CompletionView *m_completion;
|
CompletionView *m_completion;
|
||||||
History m_history;
|
|
||||||
|
|
||||||
Mode m_mode = Mode::Normal;
|
Mode m_mode = Mode::Normal;
|
||||||
QString m_pendingKeys;
|
QString m_pendingKeys;
|
||||||
|
|||||||
+75
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr auto kChars = "asdfghjkl";
|
constexpr auto kChars = "1qaz2wsx3edc4rfv5tgb6yhn7ujm8ik9ol0p";
|
||||||
|
|
||||||
// Installs window.__attHints and shows the labels. "@CHARS@" is substituted
|
// Installs window.__attHints and shows the labels. "@CHARS@" is substituted
|
||||||
// with kChars before running.
|
// with kChars before running.
|
||||||
@@ -178,6 +178,75 @@ constexpr auto kShowScript = R"JS(
|
|||||||
})();
|
})();
|
||||||
)JS";
|
)JS";
|
||||||
|
|
||||||
|
constexpr auto kFocusInputScript = R"JS(
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
const TEXT_INPUTS = new Set(['', 'text', 'search', 'email', 'url', 'tel',
|
||||||
|
'password', 'number']);
|
||||||
|
|
||||||
|
function isTextField(el) {
|
||||||
|
if (el.disabled || el.readOnly)
|
||||||
|
return false;
|
||||||
|
if (el.tagName === 'TEXTAREA')
|
||||||
|
return true;
|
||||||
|
// Only the root of an editable region, not every element inside it.
|
||||||
|
if (el.isContentEditable)
|
||||||
|
return !el.parentElement?.isContentEditable;
|
||||||
|
return el.tagName === 'INPUT' && TEXT_INPUTS.has((el.getAttribute('type') || '').toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVisible(el) {
|
||||||
|
const style = getComputedStyle(el);
|
||||||
|
if (style.visibility !== 'visible' || style.display === 'none')
|
||||||
|
return false;
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
return r.width >= 1 && r.height >= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inViewport(el) {
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
return r.bottom > 0 && r.right > 0 && r.top < innerHeight && r.left < innerWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Document order, descending into each open shadow root right after its host.
|
||||||
|
function* elements(root) {
|
||||||
|
for (const el of root.querySelectorAll('*')) {
|
||||||
|
yield el;
|
||||||
|
if (el.shadowRoot)
|
||||||
|
yield* elements(el.shadowRoot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let fallback = null;
|
||||||
|
let target = null;
|
||||||
|
for (const el of elements(document)) {
|
||||||
|
if (!isTextField(el) || !isVisible(el))
|
||||||
|
continue;
|
||||||
|
if (inViewport(el)) {
|
||||||
|
target = el;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
fallback = fallback || el;
|
||||||
|
}
|
||||||
|
target = target || fallback;
|
||||||
|
if (!target)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
target.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||||
|
target.focus();
|
||||||
|
// Put the cursor after any existing text, ready to type or edit.
|
||||||
|
if (typeof target.setSelectionRange === 'function') {
|
||||||
|
try {
|
||||||
|
target.setSelectionRange(target.value.length, target.value.length);
|
||||||
|
} catch (e) {
|
||||||
|
// type=email/number don't support selection ranges.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// From outside a shadow root, document.activeElement is only its host.
|
||||||
|
return target.getRootNode().activeElement === target;
|
||||||
|
})();
|
||||||
|
)JS";
|
||||||
|
|
||||||
QString callScript(const QString &call)
|
QString callScript(const QString &call)
|
||||||
{
|
{
|
||||||
return QStringLiteral("window.__attHints ? window.__attHints.%1 : { state: 'none' };")
|
return QStringLiteral("window.__attHints ? window.__attHints.%1 : { state: 'none' };")
|
||||||
@@ -214,4 +283,9 @@ QString clearScript()
|
|||||||
return callScript(QStringLiteral("clear()"));
|
return callScript(QStringLiteral("clear()"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString focusInputScript()
|
||||||
|
{
|
||||||
|
return QString::fromUtf8(kFocusInputScript);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace hints
|
} // namespace hints
|
||||||
|
|||||||
@@ -26,4 +26,9 @@ QString keyScript(QChar key);
|
|||||||
QString backspaceScript();
|
QString backspaceScript();
|
||||||
QString clearScript();
|
QString clearScript();
|
||||||
|
|
||||||
|
// "gi": focuses the first visible text field, preferring one in the viewport,
|
||||||
|
// and evaluates to true if there was one. Like qutebrowser, it looks into open
|
||||||
|
// shadow roots too, where sites built from web components keep their inputs.
|
||||||
|
QString focusInputScript();
|
||||||
|
|
||||||
} // namespace hints
|
} // namespace hints
|
||||||
|
|||||||
+18
-3
@@ -1,6 +1,7 @@
|
|||||||
#include "adblock.h"
|
#include "adblock.h"
|
||||||
#include "browserwindow.h"
|
#include "browserwindow.h"
|
||||||
#include "gpuworkaround.h"
|
#include "gpuworkaround.h"
|
||||||
|
#include "history.h"
|
||||||
#include "urlutil.h"
|
#include "urlutil.h"
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
@@ -82,10 +83,24 @@ int main(int argc, char *argv[])
|
|||||||
auto *adBlocker = new AdBlocker(&app);
|
auto *adBlocker = new AdBlocker(&app);
|
||||||
profile->setUrlRequestInterceptor(adBlocker);
|
profile->setUrlRequestInterceptor(adBlocker);
|
||||||
|
|
||||||
BrowserWindow window(profile, adBlocker, startUrl);
|
// Shared by every window; after QApplication, which QtSql needs.
|
||||||
window.show();
|
History history;
|
||||||
|
const Session session{profile, adBlocker, &history};
|
||||||
|
|
||||||
|
// Windows delete themselves on close; the app quits with the last one.
|
||||||
|
(new BrowserWindow(session, startUrl))->show();
|
||||||
// After the window exists, so its status bar shows the update messages.
|
// After the window exists, so its status bar shows the update messages.
|
||||||
adBlocker->start();
|
adBlocker->start();
|
||||||
|
|
||||||
return app.exec();
|
const int status = app.exec();
|
||||||
|
|
||||||
|
// A window closed last may still be waiting for its deleteLater. Delete it
|
||||||
|
// now, while the history and the profile it uses are still alive: Qt warns
|
||||||
|
// about a profile destroyed before its pages, too.
|
||||||
|
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
|
||||||
|
for (QWidget *widget : QApplication::topLevelWidgets()) {
|
||||||
|
if (qobject_cast<BrowserWindow *>(widget))
|
||||||
|
delete widget;
|
||||||
|
}
|
||||||
|
return status;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user