make bar look like the one from qutebrowser

This commit is contained in:
2026-10-17 13:53:01 +02:00
parent 483b3f87ba
commit e202725e07
13 changed files with 354 additions and 96 deletions
+3 -2
View File
@@ -34,7 +34,7 @@ The only tests are the Rust crate's (`nix develop -c sh -c 'cd adblock && cargo
4. Hint mode swallows every key. 4. Hint mode swallows every key.
5. Normal mode goes to `handleNormalModeKey`. Bindings are `if (text == "x")` chains, and two-key sequences (`gg`) use `m_pendingKeys`. 5. Normal mode goes to `handleNormalModeKey`. Bindings are `if (text == "x")` chains, and two-key sequences (`gg`) use `m_pendingKeys`.
- **Never change the page's size from UI chrome.** Every view resize makes the page re-layout, which on YouTube visibly jumps the player. - **Never change the page's size from UI chrome.** Every view resize makes the page re-layout, which on YouTube visibly jumps the player.
- `CompletionView` is not in the layout. It floats over the bottom of the view (`placeCompletion`, re-run on the view's `Resize`). - `CompletionView` and `MessageView` are not in the layout. They float over the bottom of the view (`placeCompletion` / `placeMessages`, re-run on the view's `Resize`; `MessageView` emits `changed()` when a message comes or goes).
- `StatusBar` and `CommandLine` share one fixed height. - `StatusBar` and `CommandLine` share one fixed height.
- Open the command line only through `openCommandLine`, which hides the status bar *before* showing the command line. Showing a widget re-runs the layout at once, so the other order shrinks and regrows the page. - Open the command line only through `openCommandLine`, which hides the status bar *before* showing the command line. Showing a widget re-runs the layout at once, so the other order shrinks and regrows the page.
- **`setMode` is where entering and leaving a mode takes effect.** It updates the status line and focus. Leaving `Hint` also runs the hint-clear script. - **`setMode` is where entering and leaving a mode takes effect.** It updates the status line and focus. Leaving `Hint` also runs the hint-clear script.
@@ -65,7 +65,8 @@ The only tests are the Rust crate's (`nix develop -c sh -c 'cd adblock && cargo
- **Hardening** (`src/hardening.cpp`): `applyToProfile` sets the restrictive `QWebEngineSettings` and the third-party cookie filter. The filter runs on the network thread and must stay a pure function. `main.cpp` applies it right after creating the profile, and warns in the first window if `sandboxDisabledReason` finds the sandbox off. `StatusBar::setUrl` takes a `QUrl` so it can mark plain-HTTP pages (`insecure` property). - **Hardening** (`src/hardening.cpp`): `applyToProfile` sets the restrictive `QWebEngineSettings` and the third-party cookie filter. The filter runs on the network thread and must stay a pure function. `main.cpp` applies it right after creating the profile, and warns in the first window if `sandboxDisabledReason` finds the sandbox off. `StatusBar::setUrl` takes a `QUrl` so it can mark plain-HTTP pages (`insecure` property).
- **User agent** (`src/useragent.cpp`): `useragent::apply` sets the profile's UA string *and* its `QWebEngineClientHints`, so the two don't contradict each other. Chromium sends the low-entropy hints (brands, platform, mobile) even with `setAllClientHintsEnabled(false)`, so every preset sets a platform; the brand ("Google Chrome", "Microsoft Edge") goes in through `setFullVersionList`. Chrome-based presets use the engine's own major version. `:useragent` completion reuses `CompletionView` with name / string columns. - **User agent** (`src/useragent.cpp`): `useragent::apply` sets the profile's UA string *and* its `QWebEngineClientHints`, so the two don't contradict each other. Chromium sends the low-entropy hints (brands, platform, mobile) even with `setAllClientHintsEnabled(false)`, so every preset sets a platform; the brand ("Google Chrome", "Microsoft Edge") goes in through `setFullVersionList`. Chrome-based presets use the engine's own major version. `:useragent` completion reuses `CompletionView` with name / string columns.
- **Settings** live as constants in `src/config.h` (`config::kZoomDefault`, `config::kZoomLevels`, `kBlockThirdPartyCookies`, `kPdfViewer`, `kUserAgent`, `kScrollStepPx`), named after qutebrowser's settings, until there's a config file. Zoom is per window: `setZoom` / `zoomStep` in `BrowserWindow`, applied before the first load. QtWebEngine keeps a view's zoom across navigations, so it doesn't need re-applying. - **Settings** live as constants in `src/config.h` (`config::kZoomDefault`, `config::kZoomLevels`, `kBlockThirdPartyCookies`, `kPdfViewer`, `kUserAgent`, `kScrollStepPx`), named after qutebrowser's settings, until there's a config file. Zoom is per window: `setZoom` / `zoomStep` in `BrowserWindow`, applied before the first load. QtWebEngine keeps a view's zoom across navigations, so it doesn't need re-applying.
- 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`, with qutebrowser's default colours; `@FONT@` is replaced with `config::kFont` (the hint script does the same in its CSS). Stylesheet fonts and colours don't propagate to child widgets, so rules name the labels (`QWidget#statusBar QLabel`). Widgets are styled by `objectName` and by dynamic properties, re-polished on change: the status bar's `mode` (insert turns it green), the URL label's `state` (`https`/`error`/`hover`) and a message's `level`.
- **Status bar** (`StatusBar`) is laid out like qutebrowser's: mode, key string (`m_pendingKeys`), URL (an eliding label, so a long URL can't widen the window), scroll position, progress bar. All messages go through `BrowserWindow::showMessage` to the `MessageView`, never to the bar. The scroll position comes from `QWebEnginePage::scrollPosition`/`contentsSize`, which are in *zoomed* pixels like the view's height, not CSS pixels.
**Windows, not tabs.** att_browser 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. **Windows, not tabs.** att_browser 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.
+2
View File
@@ -43,6 +43,8 @@ qt_add_executable(att_browser
src/hardening.h src/hardening.h
src/hints.cpp src/hints.cpp
src/hints.h src/hints.h
src/messageview.cpp
src/messageview.h
src/history.cpp src/history.cpp
src/history.h src/history.h
src/statusbar.cpp src/statusbar.cpp
+17 -2
View File
@@ -45,6 +45,17 @@ Like qutebrowser, keys mean different things depending on the mode:
leaves fullscreen when a page (e.g. a YouTube video) is fullscreen. The status bar leaves fullscreen when a page (e.g. a YouTube video) is fullscreen. The status bar
is hidden while a page is fullscreen. is hidden while a page is fullscreen.
### Status bar
Laid out and coloured like qutebrowser's defaults: black, turning dark green in
insert mode. On the left is the mode (`-- INSERT MODE --`, `-- HINT MODE --`),
on the right the keys typed so far (e.g. `g`), the URL, the scroll position
(`[top]`, `[42%]`, `[bot]`) and, while a page loads, a progress bar. The URL is
white while loading and for plain `http://`, green once an HTTPS page has
loaded, orange if the load failed, and aqua while it shows a link under the
mouse. Messages stack above the bar (info on black, errors on red) and vanish
after three seconds. All of it uses the `fonts` setting (14pt monospace).
> Note: unlike qutebrowser, att_browser does not yet auto-switch to insert mode when you > Note: unlike qutebrowser, att_browser does not yet auto-switch to insert mode when you
> click into a text field on a page — press `i` first. Doing it automatically > click into a text field on a page — press `i` first. Doing it automatically
> needs a JS focus-tracking script over QWebChannel; see *Next steps*. > needs a JS focus-tracking script over QWebChannel; see *Next steps*.
@@ -158,7 +169,8 @@ On top of Chromium's own protections, att_browser:
- keeps WebRTC from revealing your LAN address, and doesn't let scripts open - keeps WebRTC from revealing your LAN address, and doesn't let scripts open
pop-up windows. pop-up windows.
- pins the safe defaults for mixed content, clipboard access and plugins. - pins the safe defaults for mixed content, clipboard access and plugins.
- shows plain `http://` URLs in orange in the status bar. - shows the URL in green in the status bar only once an HTTPS page has loaded;
plain `http://` stays white, as in qutebrowser.
- only accepts Greasemonkey `@require` files over HTTPS. A `@require` URL may pin - only accepts Greasemonkey `@require` files over HTTPS. A `@require` URL may pin
its content as in Tampermonkey (`…#sha256=<hex or base64>` or `…#sha256-<base64>`), its content as in Tampermonkey (`…#sha256=<hex or base64>` or `…#sha256-<base64>`),
and the script is skipped if the file doesn't match. and the script is skipped if the file doesn't match.
@@ -206,6 +218,8 @@ their qutebrowser equivalents; change them there and rebuild:
| `content.cookies.accept` | no third-party | `kBlockThirdPartyCookies` | | `content.cookies.accept` | no third-party | `kBlockThirdPartyCookies` |
| `content.pdfjs` | on | `kPdfViewer`: Chromium's PDF viewer | | `content.pdfjs` | on | `kPdfViewer`: Chromium's PDF viewer |
| `j` / `k` step (`scroll-px`) | 25 CSS px | `kScrollStepPx`; grows with the zoom | | `j` / `k` step (`scroll-px`) | 25 CSS px | `kScrollStepPx`; grows with the zoom |
| `fonts.*` | 14pt monospace | `kFont`: bar, completion, messages, hints, context menu |
| `messages.timeout` | 3000 ms | `kMessageTimeoutMs` |
| `content.headers.user_agent` | `chrome-linux` | `kUserAgent`: the preset att_browser starts with | | `content.headers.user_agent` | `chrome-linux` | `kUserAgent`: the preset att_browser starts with |
## Commands ## Commands
@@ -233,7 +247,8 @@ an existing local path, or free text, which goes to DuckDuckGo.
| `src/hardening` | profile security settings, sandbox check | | `src/hardening` | profile security settings, sandbox check |
| `src/history` | SQLite browsing history | | `src/history` | SQLite browsing history |
| `src/completionview`| the `:open` / `:useragent` completion list | | `src/completionview`| the `:open` / `:useragent` completion list |
| `src/statusbar` | mode / URL / progress line | | `src/statusbar` | mode / keys / URL / scroll / progress line |
| `src/messageview` | the message stack above the status bar |
| `src/adblock` | request interceptor around the adblock engine | | `src/adblock` | request interceptor around the adblock engine |
| `adblock/` | Rust crate: C API over adblock-rust, built by Corrosion | | `adblock/` | Rust crate: C API over adblock-rust, built by Corrosion |
| `src/hints` | the injected hint-mode script | | `src/hints` | the injected hint-mode script |
+67 -26
View File
@@ -6,6 +6,7 @@
#include "completionview.h" #include "completionview.h"
#include "greasemonkey.h" #include "greasemonkey.h"
#include "hints.h" #include "hints.h"
#include "messageview.h"
#include "history.h" #include "history.h"
#include "statusbar.h" #include "statusbar.h"
#include "urlutil.h" #include "urlutil.h"
@@ -61,6 +62,7 @@ BrowserWindow::BrowserWindow(const Session &session, const QUrl &startUrl, QWidg
, 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))
, m_messages(new MessageView(this))
{ {
setAttribute(Qt::WA_DeleteOnClose); setAttribute(Qt::WA_DeleteOnClose);
setupUi(); setupUi();
@@ -75,8 +77,9 @@ BrowserWindow::BrowserWindow(const Session &session, const QUrl &startUrl, QWidg
}); });
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_session.adBlocker, &AdBlocker::message, m_status, &StatusBar::showMessage); connect(m_session.adBlocker, &AdBlocker::message, this, &BrowserWindow::showMessage);
connect(m_session.greasemonkey, &Greasemonkey::message, m_status, &StatusBar::showMessage); connect(m_session.greasemonkey, &Greasemonkey::message, this, &BrowserWindow::showMessage);
connect(m_messages, &MessageView::changed, this, &BrowserWindow::placeMessages);
qApp->installEventFilter(this); qApp->installEventFilter(this);
@@ -103,9 +106,11 @@ void BrowserWindow::setupUi()
// Nothing that comes and goes may change the page's size: every resize // 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 // 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 // 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 // taking space in the layout (see placeCompletion), as do the messages
// (placeMessages), and the status bar and
// the command line, which replace each other, get the same fixed height. // the command line, which replace each other, get the same fixed height.
m_completion->setParent(central); m_completion->setParent(central);
m_messages->setParent(central);
m_status->ensurePolished(); m_status->ensurePolished();
m_commandLine->ensurePolished(); m_commandLine->ensurePolished();
const int barHeight = qMax(m_status->sizeHint().height(), m_commandLine->sizeHint().height()); const int barHeight = qMax(m_status->sizeHint().height(), m_commandLine->sizeHint().height());
@@ -124,6 +129,11 @@ void BrowserWindow::connectView()
setMode(Mode::Normal); setMode(Mode::Normal);
}); });
connect(m_view, &QWebEngineView::loadProgress, m_status, &StatusBar::setProgress); connect(m_view, &QWebEngineView::loadProgress, m_status, &StatusBar::setProgress);
connect(m_view->page(), &QWebEnginePage::linkHovered, m_status, &StatusBar::setHoverUrl);
connect(m_view->page(), &QWebEnginePage::scrollPositionChanged, this,
&BrowserWindow::updateScrollPercent);
connect(m_view->page(), &QWebEnginePage::contentsSizeChanged, this,
&BrowserWindow::updateScrollPercent);
// Off by default in QtWebEngine: without it, element.requestFullscreen() // Off by default in QtWebEngine: without it, element.requestFullscreen()
// (YouTube's fullscreen button, double-clicking a video) silently fails. // (YouTube's fullscreen button, double-clicking a video) silently fails.
@@ -140,10 +150,11 @@ void BrowserWindow::connectView()
}); });
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);
if (!ok) { // A failed load turns the URL orange, as in qutebrowser; Chromium's
m_status->showMessage(QStringLiteral("Failed to load page"), true); // error page says why.
m_status->setLoadFinished(ok);
if (!ok)
return; return;
}
// 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_session.history->addVisit(m_view->url(), m_view->title()); m_session.history->addVisit(m_view->url(), m_view->title());
@@ -152,7 +163,7 @@ void BrowserWindow::connectView()
void BrowserWindow::showMessage(const QString &text, bool isError) void BrowserWindow::showMessage(const QString &text, bool isError)
{ {
m_status->showMessage(text, isError); m_messages->showMessage(text, isError);
} }
void BrowserWindow::setMode(Mode mode) void BrowserWindow::setMode(Mode mode)
@@ -162,6 +173,7 @@ void BrowserWindow::setMode(Mode mode)
m_mode = mode; m_mode = mode;
m_pendingKeys.clear(); m_pendingKeys.clear();
m_status->setKeyString(QString());
if (mode != Mode::Command) if (mode != Mode::Command)
m_completion->setEntries({}); m_completion->setEntries({});
@@ -212,8 +224,13 @@ void BrowserWindow::updateStatusVisibility()
bool BrowserWindow::eventFilter(QObject *watched, QEvent *event) bool BrowserWindow::eventFilter(QObject *watched, QEvent *event)
{ {
if (watched == m_view && event->type() == QEvent::Resize && m_completion->isVisible()) if (watched == m_view && event->type() == QEvent::Resize) {
placeCompletion(); if (m_completion->isVisible())
placeCompletion();
if (m_messages->isVisible())
placeMessages();
updateScrollPercent();
}
if (event->type() != QEvent::KeyPress || !isAncestorOf(qobject_cast<QWidget *>(watched))) if (event->type() != QEvent::KeyPress || !isAncestorOf(qobject_cast<QWidget *>(watched)))
return QMainWindow::eventFilter(watched, event); return QMainWindow::eventFilter(watched, event);
@@ -243,7 +260,9 @@ bool BrowserWindow::eventFilter(QObject *watched, QEvent *event)
return true; return true;
} }
if (handleNormalModeKey(key)) const bool handled = handleNormalModeKey(key);
m_status->setKeyString(m_pendingKeys);
if (handled)
return true; return true;
return QMainWindow::eventFilter(watched, event); return QMainWindow::eventFilter(watched, event);
@@ -366,7 +385,7 @@ bool BrowserWindow::handleNormalModeKey(QKeyEvent *event)
if (text == QLatin1String("y")) { if (text == QLatin1String("y")) {
const QString url = m_view->url().toString(); const QString url = m_view->url().toString();
qApp->clipboard()->setText(url); qApp->clipboard()->setText(url);
m_status->showMessage(QStringLiteral("Yanked %1").arg(url)); showMessage(QStringLiteral("Yanked %1").arg(url));
return true; return true;
} }
if (text == QLatin1String("+")) { if (text == QLatin1String("+")) {
@@ -415,7 +434,7 @@ void BrowserWindow::focusInput()
if (focused.toBool()) if (focused.toBool())
setMode(Mode::Insert); setMode(Mode::Insert);
else else
m_status->showMessage(QStringLiteral("No input field found"), true); showMessage(QStringLiteral("No input field found"), true);
}); });
} }
@@ -439,7 +458,7 @@ void BrowserWindow::handleHintResult(const QVariantMap &result)
if (state == QLatin1String("shown")) { if (state == QLatin1String("shown")) {
if (result.value(QStringLiteral("count")).toInt() == 0) { if (result.value(QStringLiteral("count")).toInt() == 0) {
setMode(Mode::Normal); setMode(Mode::Normal);
m_status->showMessage(QStringLiteral("No elements found"), true); showMessage(QStringLiteral("No elements found"), true);
} }
} else if (state == QLatin1String("follow")) { } else if (state == QLatin1String("follow")) {
const QPointF pos(result.value(QStringLiteral("x")).toDouble(), const QPointF pos(result.value(QStringLiteral("x")).toDouble(),
@@ -450,7 +469,7 @@ void BrowserWindow::handleHintResult(const QVariantMap &result)
setMode(Mode::Insert); setMode(Mode::Insert);
} else if (state == QLatin1String("gone")) { } else if (state == QLatin1String("gone")) {
setMode(Mode::Normal); setMode(Mode::Normal);
m_status->showMessage(QStringLiteral("Element went away"), true); showMessage(QStringLiteral("Element went away"), true);
} else if (state == QLatin1String("none") || state.isEmpty()) { } else if (state == QLatin1String("none") || state.isEmpty()) {
// The script failed, or the page navigated and took the hints with it. // The script failed, or the page navigated and took the hints with it.
setMode(Mode::Normal); setMode(Mode::Normal);
@@ -500,7 +519,7 @@ void BrowserWindow::runCommand(const QString &command)
} else if (name == QLatin1String("version")) { } else if (name == QLatin1String("version")) {
// The Chromium security patch level is what says how far behind // The Chromium security patch level is what says how far behind
// Chrome's fixes this build is; "nix flake update" moves it forward. // 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") showMessage(QStringLiteral("att_browser %1, Qt %2, Chromium %3, security patches %4")
.arg(QCoreApplication::applicationVersion(), .arg(QCoreApplication::applicationVersion(),
QString::fromLatin1(qVersion()), QString::fromLatin1(qVersion()),
QString::fromLatin1(qWebEngineChromiumVersion()), QString::fromLatin1(qWebEngineChromiumVersion()),
@@ -518,7 +537,7 @@ void BrowserWindow::runCommand(const QString &command)
if (ok && percent >= kZoomMin * 100 && percent <= kZoomMax * 100) if (ok && percent >= kZoomMin * 100 && percent <= kZoomMax * 100)
setZoom(percent / 100); setZoom(percent / 100);
else else
m_status->showMessage(QStringLiteral("Zoom level must be %1% to %2%: %3") showMessage(QStringLiteral("Zoom level must be %1% to %2%: %3")
.arg(kZoomMin * 100).arg(kZoomMax * 100).arg(args), .arg(kZoomMin * 100).arg(kZoomMax * 100).arg(args),
true); true);
} }
@@ -528,22 +547,22 @@ void BrowserWindow::runCommand(const QString &command)
zoomStep(-1); zoomStep(-1);
} else if (name == QLatin1String("useragent") || name == QLatin1String("ua")) { } else if (name == QLatin1String("useragent") || name == QLatin1String("ua")) {
if (args.isEmpty()) { if (args.isEmpty()) {
m_status->showMessage(QStringLiteral("User agent: %1").arg(useragent::current())); showMessage(QStringLiteral("User agent: %1").arg(useragent::current()));
} else if (useragent::apply(m_session.profile, args)) { } else if (useragent::apply(m_session.profile, args)) {
// Requests use it at once; navigator.userAgent changes with the // Requests use it at once; navigator.userAgent changes with the
// next load, so say so rather than leave the page inconsistent. // next load, so say so rather than leave the page inconsistent.
m_status->showMessage( showMessage(
QStringLiteral("User agent: %1 (applies from the next page load)").arg(args)); QStringLiteral("User agent: %1 (applies from the next page load)").arg(args));
} else { } else {
m_status->showMessage(QStringLiteral("Unknown user agent: %1").arg(args), true); showMessage(QStringLiteral("Unknown user agent: %1").arg(args), true);
} }
} else if (name == QLatin1String("history-clear")) { } else if (name == QLatin1String("history-clear")) {
if (m_session.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")); showMessage(QStringLiteral("History cleared"));
} else { } else {
m_status->showMessage(QStringLiteral("Could not clear history"), true); showMessage(QStringLiteral("Could not clear history"), true);
} }
} else if (name == QLatin1String("greasemonkey-reload")) { } else if (name == QLatin1String("greasemonkey-reload")) {
m_session.greasemonkey->reload(); m_session.greasemonkey->reload();
@@ -551,10 +570,10 @@ void BrowserWindow::runCommand(const QString &command)
m_session.adBlocker->update(); m_session.adBlocker->update();
} else if (name == QLatin1String("adblock-toggle")) { } else if (name == QLatin1String("adblock-toggle")) {
m_session.adBlocker->setEnabled(!m_session.adBlocker->isEnabled()); m_session.adBlocker->setEnabled(!m_session.adBlocker->isEnabled());
m_status->showMessage(m_session.adBlocker->isEnabled() ? QStringLiteral("Adblock enabled") 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); showMessage(QStringLiteral("Unknown command: %1").arg(name), true);
} }
} }
@@ -591,6 +610,28 @@ void BrowserWindow::updateCompletion()
placeCompletion(); placeCompletion();
} }
void BrowserWindow::placeMessages()
{
// Bottom-aligned over the page, just above the status bar, full width.
const QRect page = m_view->geometry();
m_messages->setFixedWidth(page.width());
m_messages->adjustSize();
const int height = qMin(m_messages->height(), page.height());
m_messages->setGeometry(page.left(), page.bottom() + 1 - height, page.width(), height);
m_messages->raise();
}
void BrowserWindow::updateScrollPercent()
{
// scrollPosition and contentsSize are in zoomed pixels, the same units as
// the view's own height (measured: at 125% the bottom of a page is exactly
// contentsSize - view height).
const QWebEnginePage *page = m_view->page();
const double scrollable = page->contentsSize().height() - m_view->height();
m_status->setScrollPercent(
scrollable < 1 ? 0 : qRound(100 * qBound(0.0, page->scrollPosition().y() / scrollable, 1.0)));
}
void BrowserWindow::placeCompletion() void BrowserWindow::placeCompletion()
{ {
// Bottom-aligned over the page, just above the command line. // Bottom-aligned over the page, just above the command line.
@@ -621,7 +662,7 @@ 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); showMessage(QStringLiteral("Invalid URL: %1").arg(input), true);
return; return;
} }
if (newWindow) { if (newWindow) {
@@ -680,7 +721,7 @@ void BrowserWindow::setZoom(double factor, bool announce)
const double zoom = qBound(kZoomMin, factor, kZoomMax); const double zoom = qBound(kZoomMin, factor, kZoomMax);
m_view->setZoomFactor(zoom); m_view->setZoomFactor(zoom);
if (announce) if (announce)
m_status->showMessage(QStringLiteral("Zoom level: %1%").arg(qRound(zoom * 100))); showMessage(QStringLiteral("Zoom level: %1%").arg(qRound(zoom * 100)));
} }
void BrowserWindow::zoomStep(int direction) void BrowserWindow::zoomStep(int direction)
@@ -705,7 +746,7 @@ void BrowserWindow::zoomStep(int direction)
} }
} }
} }
m_status->showMessage(QStringLiteral("Zoom level: %1% (limit)").arg(qRound(current * 100))); showMessage(QStringLiteral("Zoom level: %1% (limit)").arg(qRound(current * 100)));
} }
void BrowserWindow::focusView() void BrowserWindow::focusView()
+4
View File
@@ -9,6 +9,7 @@ class AdBlocker;
class CommandLine; class CommandLine;
class CompletionView; class CompletionView;
class Greasemonkey; class Greasemonkey;
class MessageView;
class History; class History;
class StatusBar; class StatusBar;
class QWebEngineProfile; class QWebEngineProfile;
@@ -64,6 +65,8 @@ private:
void updateCompletion(); void updateCompletion();
void cycleCompletion(int step); void cycleCompletion(int step);
void placeCompletion(); void placeCompletion();
void placeMessages();
void updateScrollPercent();
void openUrl(const QString &input, bool newWindow); void openUrl(const QString &input, bool newWindow);
void scrollBy(int dx, int dy); void scrollBy(int dx, int dy);
void scrollByCssPx(int dx, int dy); void scrollByCssPx(int dx, int dy);
@@ -80,6 +83,7 @@ private:
StatusBar *m_status; StatusBar *m_status;
CommandLine *m_commandLine; CommandLine *m_commandLine;
CompletionView *m_completion; CompletionView *m_completion;
MessageView *m_messages;
Mode m_mode = Mode::Normal; Mode m_mode = Mode::Normal;
QString m_pendingKeys; QString m_pendingKeys;
+1
View File
@@ -17,6 +17,7 @@ CompletionView::CompletionView(QWidget *parent)
setHeaderHidden(true); setHeaderHidden(true);
setRootIsDecorated(false); setRootIsDecorated(false);
setUniformRowHeights(true); setUniformRowHeights(true);
setAlternatingRowColors(true);
setFrameShape(QFrame::NoFrame); setFrameShape(QFrame::NoFrame);
setSelectionMode(QAbstractItemView::SingleSelection); setSelectionMode(QAbstractItemView::SingleSelection);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+8
View File
@@ -17,6 +17,14 @@ inline constexpr std::array kZoomLevels = {
1.25, 1.50, 1.75, 2.00, 2.50, 3.00, 4.00, 5.00, 1.25, 1.50, 1.75, 2.00, 2.50, 3.00, 4.00, 5.00,
}; };
// fonts.*: qutebrowser's fonts.statusbar, fonts.completion.*, fonts.hints,
// fonts.messages.* and fonts.prompts, as one Qt / CSS font shorthand. Used by
// the status bar, command line, completion, messages and hint labels.
inline constexpr auto kFont = "14pt monospace";
// messages.timeout: how long a message stays up, in milliseconds.
inline constexpr int kMessageTimeoutMs = 3000;
// What "j" / "k" do: qutebrowser's "scroll-px 0 25" / "scroll-px 0 -25". In // What "j" / "k" do: qutebrowser's "scroll-px 0 25" / "scroll-px 0 -25". In
// CSS pixels, so the step grows with the zoom like qutebrowser's does. // CSS pixels, so the step grows with the zoom like qutebrowser's does.
inline constexpr int kScrollStepPx = 25; inline constexpr int kScrollStepPx = 25;
+8 -4
View File
@@ -1,11 +1,13 @@
#include "hints.h" #include "hints.h"
#include "config.h"
namespace { namespace {
constexpr auto kChars = "1qaz2wsx3edc4rfv5tgb6yhn7ujm8ik9ol0p"; constexpr auto kChars = "1qaz2wsx3edc4rfv5tgb6yhn7ujm8ik9ol0p";
// Installs window.__attHints and shows the labels. "@CHARS@" is substituted // Installs window.__attHints and shows the labels. "@CHARS@" and "@FONT@" are
// with kChars before running. // substituted with kChars and config::kFont before running.
constexpr auto kShowScript = R"JS( constexpr auto kShowScript = R"JS(
(function () { (function () {
'use strict'; 'use strict';
@@ -90,7 +92,7 @@ constexpr auto kShowScript = R"JS(
border: 1px solid #c38a22; border: 1px solid #c38a22;
border-radius: 3px; border-radius: 3px;
color: #000; color: #000;
font: bold 12px monospace; font: @FONT@;
line-height: normal; line-height: normal;
text-transform: uppercase; text-transform: uppercase;
white-space: nowrap; white-space: nowrap;
@@ -264,7 +266,9 @@ QString chars()
QString showScript() QString showScript()
{ {
return QString::fromUtf8(kShowScript).replace(QLatin1String("@CHARS@"), chars()); return QString::fromUtf8(kShowScript)
.replace(QLatin1String("@CHARS@"), chars())
.replace(QLatin1String("@FONT@"), QString::fromLatin1(config::kFont));
} }
QString keyScript(QChar key) QString keyScript(QChar key)
+55 -22
View File
@@ -18,36 +18,68 @@ namespace {
constexpr auto kDefaultUrl = "https://duckduckgo.com/"; constexpr auto kDefaultUrl = "https://duckduckgo.com/";
// qutebrowser's default colours. @FONT@ is config::kFont.
constexpr auto kStyleSheet = R"( constexpr auto kStyleSheet = R"(
/* Stylesheet fonts and colours don't propagate to child widgets, so the
status bar's labels are named explicitly. */
QWidget#statusBar QLabel, QLineEdit#commandLine, QTreeWidget#completion, QLabel#message {
font: @FONT@;
color: white;
}
QWidget#statusBar, QLineEdit#commandLine { QWidget#statusBar, QLineEdit#commandLine {
background-color: #1c1c1c; background-color: black;
color: #d8d8d8;
font-family: monospace;
} }
QLabel#statusMode { /* fonts.contextmenu: the page's right-click menu. */
color: #7fbf7f; QMenu {
font-weight: bold; font: @FONT@;
} }
QLabel#statusUrl[error="true"] { QWidget#statusBar[mode="insert"] {
color: #ff6b6b; background-color: darkgreen;
} }
QLabel#statusUrl[insecure="true"] { QLabel#statusUrl[state="https"] {
color: #e5a050; color: lime;
} }
QLabel#statusProgress { QLabel#statusUrl[state="error"] {
color: #8a8a8a; color: orange;
} }
QTreeWidget#completion { QLabel#statusUrl[state="hover"] {
background-color: #262626; color: aqua;
color: #d8d8d8; }
font-family: monospace; QProgressBar#statusProgress {
border-top: 1px solid #3a3a3a; border: none;
selection-background-color: #e8c000; background-color: transparent;
selection-color: #000000; max-width: 8em;
min-width: 8em;
max-height: 0.8em;
}
QProgressBar#statusProgress::chunk {
background-color: white;
} }
QLineEdit#commandLine { QLineEdit#commandLine {
padding: 1px 4px; padding: 0px 4px;
selection-background-color: #445566; selection-background-color: #444444;
}
QTreeWidget#completion {
background-color: #333333;
alternate-background-color: #444444;
border: none;
}
QTreeWidget#completion::item:selected {
background-color: #e8c50b;
color: black;
border-top: 1px solid #bbbe03;
border-bottom: 1px solid #bbbe03;
}
QLabel#message {
padding: 0px 4px;
}
QLabel#message[level="info"] {
background-color: black;
border-bottom: 1px solid #333333;
}
QLabel#message[level="error"] {
background-color: red;
border-bottom: 1px solid #bb0000;
} }
)"; )";
@@ -94,7 +126,8 @@ int main(int argc, char *argv[])
parser.addPositionalArgument(QStringLiteral("url"), QStringLiteral("URL to open.")); parser.addPositionalArgument(QStringLiteral("url"), QStringLiteral("URL to open."));
parser.process(app); parser.process(app);
app.setStyleSheet(QString::fromUtf8(kStyleSheet)); app.setStyleSheet(QString::fromUtf8(kStyleSheet).replace(QLatin1String("@FONT@"),
QString::fromLatin1(config::kFont)));
const QStringList args = parser.positionalArguments(); const QStringList args = parser.positionalArguments();
const QUrl startUrl = args.isEmpty() ? QUrl(QString::fromLatin1(kDefaultUrl)) const QUrl startUrl = args.isEmpty() ? QUrl(QString::fromLatin1(kDefaultUrl))
+37
View File
@@ -0,0 +1,37 @@
#include "messageview.h"
#include "config.h"
#include <QLabel>
#include <QTimer>
#include <QVBoxLayout>
MessageView::MessageView(QWidget *parent)
: QWidget(parent)
, m_layout(new QVBoxLayout(this))
{
setObjectName(QStringLiteral("messages"));
setAttribute(Qt::WA_TransparentForMouseEvents);
m_layout->setContentsMargins(0, 0, 0, 0);
m_layout->setSpacing(0);
hide();
}
void MessageView::showMessage(const QString &text, bool isError)
{
auto *label = new QLabel(text, this);
label->setObjectName(QStringLiteral("message"));
label->setProperty("level", isError ? QStringLiteral("error") : QStringLiteral("info"));
label->setWordWrap(true);
label->setTextFormat(Qt::PlainText);
m_layout->addWidget(label);
show();
emit changed();
QTimer::singleShot(config::kMessageTimeoutMs, label, [this, label] {
delete label;
if (m_layout->isEmpty())
hide();
emit changed();
});
}
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <QWidget>
class QVBoxLayout;
// Messages stacked just above the status bar, like qutebrowser's: info on
// black, errors on red. Each disappears after config::kMessageTimeoutMs. It
// floats over the page rather than taking space in the layout, so a message
// never resizes the page. The owner positions it (BrowserWindow::placeOverlays)
// whenever it emits changed().
class MessageView : public QWidget
{
Q_OBJECT
public:
explicit MessageView(QWidget *parent = nullptr);
void showMessage(const QString &text, bool isError = false);
signals:
// A message was added or removed; the height has changed.
void changed();
private:
QVBoxLayout *m_layout;
};
+103 -29
View File
@@ -2,72 +2,146 @@
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel> #include <QLabel>
#include <QPainter>
#include <QProgressBar>
#include <QStyle> #include <QStyle>
#include <QTimer>
namespace {
// Right-aligned text that is cut short with "…" rather than widening the
// window, like qutebrowser's URL widget.
class ElidedLabel : public QLabel
{
public:
using QLabel::QLabel;
QSize minimumSizeHint() const override
{
return {0, QLabel::minimumSizeHint().height()};
}
protected:
void paintEvent(QPaintEvent *) override
{
QPainter painter(this);
const QRect area = contentsRect();
painter.setPen(palette().color(foregroundRole()));
painter.drawText(area, Qt::AlignRight | Qt::AlignVCenter,
fontMetrics().elidedText(text(), Qt::ElideRight, area.width()));
}
};
// Re-polish so the stylesheet picks up a changed dynamic property.
void setStyleProperty(QWidget *widget, const char *name, const QString &value)
{
if (widget->property(name).toString() == value)
return;
widget->setProperty(name, value);
widget->style()->unpolish(widget);
widget->style()->polish(widget);
}
} // namespace
StatusBar::StatusBar(QWidget *parent) StatusBar::StatusBar(QWidget *parent)
: QWidget(parent) : QWidget(parent)
, m_mode(new QLabel(this)) , m_mode(new QLabel(this))
, m_url(new QLabel(this)) , m_keys(new QLabel(this))
, m_progress(new QLabel(this)) , m_url(new ElidedLabel(this))
, m_scroll(new QLabel(this))
, m_progress(new QProgressBar(this))
{ {
setObjectName(QStringLiteral("statusBar")); setObjectName(QStringLiteral("statusBar"));
// A plain QWidget subclass only paints a stylesheet background with this.
setAttribute(Qt::WA_StyledBackground);
m_mode->setObjectName(QStringLiteral("statusMode")); m_mode->setObjectName(QStringLiteral("statusMode"));
m_keys->setObjectName(QStringLiteral("statusKeys"));
m_url->setObjectName(QStringLiteral("statusUrl")); m_url->setObjectName(QStringLiteral("statusUrl"));
m_scroll->setObjectName(QStringLiteral("statusScroll"));
m_progress->setObjectName(QStringLiteral("statusProgress")); m_progress->setObjectName(QStringLiteral("statusProgress"));
m_url->setTextInteractionFlags(Qt::NoTextInteraction); m_url->setTextInteractionFlags(Qt::NoTextInteraction);
m_url->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
m_progress->setTextVisible(false);
m_progress->setRange(0, 100);
m_progress->hide();
auto *layout = new QHBoxLayout(this); auto *layout = new QHBoxLayout(this);
layout->setContentsMargins(4, 1, 4, 1); layout->setContentsMargins(4, 0, 4, 0);
layout->setSpacing(8); layout->setSpacing(8);
layout->addWidget(m_mode); layout->addWidget(m_mode);
layout->addWidget(m_keys);
layout->addWidget(m_url, 1); layout->addWidget(m_url, 1);
layout->addWidget(m_scroll);
layout->addWidget(m_progress); layout->addWidget(m_progress);
setMode(QString());
setScrollPercent(0);
} }
void StatusBar::setMode(const QString &mode) void StatusBar::setMode(const QString &mode)
{ {
m_mode->setText(mode.isEmpty() ? QString() m_mode->setText(mode.isEmpty() ? QString()
: QStringLiteral("-- %1 --").arg(mode.toUpper())); : QStringLiteral("-- %1 MODE --").arg(mode.toUpper()));
m_mode->setVisible(!mode.isEmpty()); setStyleProperty(this, "mode", mode);
}
void StatusBar::setKeyString(const QString &keys)
{
m_keys->setText(keys);
} }
void StatusBar::setUrl(const QUrl &url) void StatusBar::setUrl(const QUrl &url)
{ {
m_urlText = url.toDisplayString(); m_pageUrl = url;
m_insecure = url.scheme() == QLatin1String("http") || url.scheme() == QLatin1String("ws"); updateUrl();
setUrlLabelState(false, m_insecure); }
m_url->setText(m_urlText);
void StatusBar::setHoverUrl(const QString &url)
{
m_hoverUrl = url;
updateUrl();
}
void StatusBar::setLoadFinished(bool ok)
{
m_loading = false;
m_loadFailed = !ok;
updateUrl();
} }
void StatusBar::setProgress(int percent) void StatusBar::setProgress(int percent)
{ {
const bool loading = percent > 0 && percent < 100; const bool loading = percent < 100;
m_progress->setText(loading ? QStringLiteral("[%1%]").arg(percent) : QString()); if (loading && !m_loading) {
m_loading = true;
m_loadFailed = false;
updateUrl();
}
m_progress->setValue(percent);
m_progress->setVisible(loading); m_progress->setVisible(loading);
} }
void StatusBar::showMessage(const QString &message, bool isError) void StatusBar::setScrollPercent(int percent)
{ {
setUrlLabelState(isError, false); m_scroll->setText(percent <= 0 ? QStringLiteral("[top]")
m_url->setText(message); : percent >= 100 ? QStringLiteral("[bot]")
: QStringLiteral("[%1%]").arg(percent));
QTimer::singleShot(4000, this, &StatusBar::clearMessage);
} }
void StatusBar::clearMessage() void StatusBar::updateUrl()
{ {
setUrlLabelState(false, m_insecure); // qutebrowser's statusbar.url.* colours: aqua for a hovered link, orange
m_url->setText(m_urlText); // after a failed load, green once an HTTPS page has loaded, white while
} // loading and for plain HTTP (which never turns green).
QString state;
void StatusBar::setUrlLabelState(bool error, bool insecure) if (!m_hoverUrl.isEmpty())
{ state = QStringLiteral("hover");
m_url->setProperty("error", error); else if (m_loadFailed)
m_url->setProperty("insecure", insecure); state = QStringLiteral("error");
// Re-polish so the stylesheet picks up the changed properties. else if (!m_loading && m_pageUrl.scheme() == QLatin1String("https"))
m_url->style()->unpolish(m_url); state = QStringLiteral("https");
m_url->style()->polish(m_url); setStyleProperty(m_url, "state", state);
m_url->setText(m_hoverUrl.isEmpty() ? m_pageUrl.toDisplayString() : m_hoverUrl);
} }
+22 -11
View File
@@ -4,9 +4,12 @@
#include <QWidget> #include <QWidget>
class QLabel; class QLabel;
class QProgressBar;
// The line that replaces the command line when no command is being typed: // The line at the bottom of the window, laid out like qutebrowser's: the mode
// current mode on the left, URL in the middle, load progress on the right. // on the left, then on the right the keys typed so far, the URL, the scroll
// position and the load progress. Messages are not shown here but in the
// MessageView above it, as in qutebrowser.
class StatusBar : public QWidget class StatusBar : public QWidget
{ {
Q_OBJECT Q_OBJECT
@@ -14,21 +17,29 @@ class StatusBar : public QWidget
public: public:
explicit StatusBar(QWidget *parent = nullptr); explicit StatusBar(QWidget *parent = nullptr);
// Empty for normal mode. "insert" also turns the bar green.
void setMode(const QString &mode); void setMode(const QString &mode);
// Plain-HTTP URLs are shown in a warning colour: anyone on the network // A pending key sequence, e.g. "g".
// can read and change those pages. void setKeyString(const QString &keys);
void setUrl(const QUrl &url); void setUrl(const QUrl &url);
// Shows a hovered link in place of the URL; empty restores the URL.
void setHoverUrl(const QString &url);
void setLoadFinished(bool ok);
void setProgress(int percent); void setProgress(int percent);
// Transient feedback: errors, unknown commands, yanked URLs. // 0-100, shown as [top], [bot] or [NN%].
void showMessage(const QString &message, bool isError = false); void setScrollPercent(int percent);
void clearMessage();
private: private:
void setUrlLabelState(bool error, bool insecure); void updateUrl();
QLabel *m_mode; QLabel *m_mode;
QLabel *m_keys;
QLabel *m_url; QLabel *m_url;
QLabel *m_progress; QLabel *m_scroll;
QString m_urlText; QProgressBar *m_progress;
bool m_insecure = false;
QUrl m_pageUrl;
QString m_hoverUrl;
bool m_loading = false;
bool m_loadFailed = false;
}; };