From e202725e07aa0d251591b380043f61e33c52b65a Mon Sep 17 00:00:00 2001 From: Asmir A Date: Sat, 17 Oct 2026 13:53:01 +0200 Subject: [PATCH] make bar look like the one from qutebrowser --- CLAUDE.md | 5 +- CMakeLists.txt | 2 + README.md | 19 +++++- src/browserwindow.cpp | 93 +++++++++++++++++++++-------- src/browserwindow.h | 4 ++ src/completionview.cpp | 1 + src/config.h | 8 +++ src/hints.cpp | 12 ++-- src/main.cpp | 77 +++++++++++++++++------- src/messageview.cpp | 37 ++++++++++++ src/messageview.h | 27 +++++++++ src/statusbar.cpp | 132 ++++++++++++++++++++++++++++++++--------- src/statusbar.h | 33 +++++++---- 13 files changed, 354 insertions(+), 96 deletions(-) create mode 100644 src/messageview.cpp create mode 100644 src/messageview.h diff --git a/CLAUDE.md b/CLAUDE.md index 720ec89..71e3a21 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. 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. - - `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. - 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. @@ -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). - **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. -- 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. diff --git a/CMakeLists.txt b/CMakeLists.txt index e9d23b5..bbbc8a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,6 +43,8 @@ qt_add_executable(att_browser src/hardening.h src/hints.cpp src/hints.h + src/messageview.cpp + src/messageview.h src/history.cpp src/history.h src/statusbar.cpp diff --git a/README.md b/README.md index c0e972f..afddb15 100644 --- a/README.md +++ b/README.md @@ -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 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 > 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*. @@ -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 pop-up windows. - 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 its content as in Tampermonkey (`…#sha256=` or `…#sha256-`), 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.pdfjs` | on | `kPdfViewer`: Chromium's PDF viewer | | `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 | ## Commands @@ -233,7 +247,8 @@ an existing local path, or free text, which goes to DuckDuckGo. | `src/hardening` | profile security settings, sandbox check | | `src/history` | SQLite browsing history | | `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 | | `adblock/` | Rust crate: C API over adblock-rust, built by Corrosion | | `src/hints` | the injected hint-mode script | diff --git a/src/browserwindow.cpp b/src/browserwindow.cpp index 79ab8d1..3182d95 100644 --- a/src/browserwindow.cpp +++ b/src/browserwindow.cpp @@ -6,6 +6,7 @@ #include "completionview.h" #include "greasemonkey.h" #include "hints.h" +#include "messageview.h" #include "history.h" #include "statusbar.h" #include "urlutil.h" @@ -61,6 +62,7 @@ BrowserWindow::BrowserWindow(const Session &session, const QUrl &startUrl, QWidg , m_status(new StatusBar(this)) , m_commandLine(new CommandLine(this)) , m_completion(new CompletionView(this)) + , m_messages(new MessageView(this)) { setAttribute(Qt::WA_DeleteOnClose); 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, &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); + connect(m_session.adBlocker, &AdBlocker::message, this, &BrowserWindow::showMessage); + connect(m_session.greasemonkey, &Greasemonkey::message, this, &BrowserWindow::showMessage); + connect(m_messages, &MessageView::changed, this, &BrowserWindow::placeMessages); qApp->installEventFilter(this); @@ -103,9 +106,11 @@ void BrowserWindow::setupUi() // 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 + // 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. m_completion->setParent(central); + m_messages->setParent(central); m_status->ensurePolished(); m_commandLine->ensurePolished(); const int barHeight = qMax(m_status->sizeHint().height(), m_commandLine->sizeHint().height()); @@ -124,6 +129,11 @@ void BrowserWindow::connectView() setMode(Mode::Normal); }); 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() // (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) { m_status->setProgress(100); - if (!ok) { - m_status->showMessage(QStringLiteral("Failed to load page"), true); + // A failed load turns the URL orange, as in qutebrowser; Chromium's + // error page says why. + m_status->setLoadFinished(ok); + if (!ok) 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()); @@ -152,7 +163,7 @@ void BrowserWindow::connectView() void BrowserWindow::showMessage(const QString &text, bool isError) { - m_status->showMessage(text, isError); + m_messages->showMessage(text, isError); } void BrowserWindow::setMode(Mode mode) @@ -162,6 +173,7 @@ void BrowserWindow::setMode(Mode mode) m_mode = mode; m_pendingKeys.clear(); + m_status->setKeyString(QString()); if (mode != Mode::Command) m_completion->setEntries({}); @@ -212,8 +224,13 @@ void BrowserWindow::updateStatusVisibility() bool BrowserWindow::eventFilter(QObject *watched, QEvent *event) { - if (watched == m_view && event->type() == QEvent::Resize && m_completion->isVisible()) - placeCompletion(); + if (watched == m_view && event->type() == QEvent::Resize) { + if (m_completion->isVisible()) + placeCompletion(); + if (m_messages->isVisible()) + placeMessages(); + updateScrollPercent(); + } if (event->type() != QEvent::KeyPress || !isAncestorOf(qobject_cast(watched))) return QMainWindow::eventFilter(watched, event); @@ -243,7 +260,9 @@ bool BrowserWindow::eventFilter(QObject *watched, QEvent *event) return true; } - if (handleNormalModeKey(key)) + const bool handled = handleNormalModeKey(key); + m_status->setKeyString(m_pendingKeys); + if (handled) return true; return QMainWindow::eventFilter(watched, event); @@ -366,7 +385,7 @@ bool BrowserWindow::handleNormalModeKey(QKeyEvent *event) if (text == QLatin1String("y")) { const QString url = m_view->url().toString(); qApp->clipboard()->setText(url); - m_status->showMessage(QStringLiteral("Yanked %1").arg(url)); + showMessage(QStringLiteral("Yanked %1").arg(url)); return true; } if (text == QLatin1String("+")) { @@ -415,7 +434,7 @@ void BrowserWindow::focusInput() if (focused.toBool()) setMode(Mode::Insert); 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 (result.value(QStringLiteral("count")).toInt() == 0) { setMode(Mode::Normal); - m_status->showMessage(QStringLiteral("No elements found"), true); + showMessage(QStringLiteral("No elements found"), true); } } else if (state == QLatin1String("follow")) { const QPointF pos(result.value(QStringLiteral("x")).toDouble(), @@ -450,7 +469,7 @@ void BrowserWindow::handleHintResult(const QVariantMap &result) setMode(Mode::Insert); } else if (state == QLatin1String("gone")) { setMode(Mode::Normal); - m_status->showMessage(QStringLiteral("Element went away"), true); + 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); @@ -500,7 +519,7 @@ void BrowserWindow::runCommand(const QString &command) } 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") + showMessage(QStringLiteral("att_browser %1, Qt %2, Chromium %3, security patches %4") .arg(QCoreApplication::applicationVersion(), QString::fromLatin1(qVersion()), QString::fromLatin1(qWebEngineChromiumVersion()), @@ -518,7 +537,7 @@ void BrowserWindow::runCommand(const QString &command) if (ok && percent >= kZoomMin * 100 && percent <= kZoomMax * 100) setZoom(percent / 100); 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), true); } @@ -528,22 +547,22 @@ void BrowserWindow::runCommand(const QString &command) zoomStep(-1); } else if (name == QLatin1String("useragent") || name == QLatin1String("ua")) { 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)) { // Requests use it at once; navigator.userAgent changes with the // 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)); } 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")) { 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")); + showMessage(QStringLiteral("History cleared")); } else { - m_status->showMessage(QStringLiteral("Could not clear history"), true); + showMessage(QStringLiteral("Could not clear history"), true); } } else if (name == QLatin1String("greasemonkey-reload")) { m_session.greasemonkey->reload(); @@ -551,10 +570,10 @@ void BrowserWindow::runCommand(const QString &command) 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") + showMessage(m_session.adBlocker->isEnabled() ? QStringLiteral("Adblock enabled") : QStringLiteral("Adblock disabled")); } 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(); } +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() { // 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); if (!url.isValid() || url.isEmpty()) { - m_status->showMessage(QStringLiteral("Invalid URL: %1").arg(input), true); + showMessage(QStringLiteral("Invalid URL: %1").arg(input), true); return; } if (newWindow) { @@ -680,7 +721,7 @@ 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))); + showMessage(QStringLiteral("Zoom level: %1%").arg(qRound(zoom * 100))); } 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() diff --git a/src/browserwindow.h b/src/browserwindow.h index cff3f99..ec0cb31 100644 --- a/src/browserwindow.h +++ b/src/browserwindow.h @@ -9,6 +9,7 @@ class AdBlocker; class CommandLine; class CompletionView; class Greasemonkey; +class MessageView; class History; class StatusBar; class QWebEngineProfile; @@ -64,6 +65,8 @@ private: void updateCompletion(); void cycleCompletion(int step); void placeCompletion(); + void placeMessages(); + void updateScrollPercent(); void openUrl(const QString &input, bool newWindow); void scrollBy(int dx, int dy); void scrollByCssPx(int dx, int dy); @@ -80,6 +83,7 @@ private: StatusBar *m_status; CommandLine *m_commandLine; CompletionView *m_completion; + MessageView *m_messages; Mode m_mode = Mode::Normal; QString m_pendingKeys; diff --git a/src/completionview.cpp b/src/completionview.cpp index d66a909..472419a 100644 --- a/src/completionview.cpp +++ b/src/completionview.cpp @@ -17,6 +17,7 @@ CompletionView::CompletionView(QWidget *parent) setHeaderHidden(true); setRootIsDecorated(false); setUniformRowHeights(true); + setAlternatingRowColors(true); setFrameShape(QFrame::NoFrame); setSelectionMode(QAbstractItemView::SingleSelection); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); diff --git a/src/config.h b/src/config.h index 88c9ec0..ea6212d 100644 --- a/src/config.h +++ b/src/config.h @@ -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, }; +// 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 // CSS pixels, so the step grows with the zoom like qutebrowser's does. inline constexpr int kScrollStepPx = 25; diff --git a/src/hints.cpp b/src/hints.cpp index 5eeb047..88ff539 100644 --- a/src/hints.cpp +++ b/src/hints.cpp @@ -1,11 +1,13 @@ #include "hints.h" +#include "config.h" + namespace { constexpr auto kChars = "1qaz2wsx3edc4rfv5tgb6yhn7ujm8ik9ol0p"; -// Installs window.__attHints and shows the labels. "@CHARS@" is substituted -// with kChars before running. +// Installs window.__attHints and shows the labels. "@CHARS@" and "@FONT@" are +// substituted with kChars and config::kFont before running. constexpr auto kShowScript = R"JS( (function () { 'use strict'; @@ -90,7 +92,7 @@ constexpr auto kShowScript = R"JS( border: 1px solid #c38a22; border-radius: 3px; color: #000; - font: bold 12px monospace; + font: @FONT@; line-height: normal; text-transform: uppercase; white-space: nowrap; @@ -264,7 +266,9 @@ QString chars() 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) diff --git a/src/main.cpp b/src/main.cpp index 52dfd17..7a4c13e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -18,36 +18,68 @@ namespace { constexpr auto kDefaultUrl = "https://duckduckgo.com/"; +// qutebrowser's default colours. @FONT@ is config::kFont. 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 { - background-color: #1c1c1c; - color: #d8d8d8; - font-family: monospace; + background-color: black; } - QLabel#statusMode { - color: #7fbf7f; - font-weight: bold; + /* fonts.contextmenu: the page's right-click menu. */ + QMenu { + font: @FONT@; } - QLabel#statusUrl[error="true"] { - color: #ff6b6b; + QWidget#statusBar[mode="insert"] { + background-color: darkgreen; } - QLabel#statusUrl[insecure="true"] { - color: #e5a050; + QLabel#statusUrl[state="https"] { + color: lime; } - QLabel#statusProgress { - color: #8a8a8a; + QLabel#statusUrl[state="error"] { + color: orange; } - QTreeWidget#completion { - background-color: #262626; - color: #d8d8d8; - font-family: monospace; - border-top: 1px solid #3a3a3a; - selection-background-color: #e8c000; - selection-color: #000000; + QLabel#statusUrl[state="hover"] { + color: aqua; + } + QProgressBar#statusProgress { + border: none; + background-color: transparent; + max-width: 8em; + min-width: 8em; + max-height: 0.8em; + } + QProgressBar#statusProgress::chunk { + background-color: white; } QLineEdit#commandLine { - padding: 1px 4px; - selection-background-color: #445566; + padding: 0px 4px; + 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.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 QUrl startUrl = args.isEmpty() ? QUrl(QString::fromLatin1(kDefaultUrl)) diff --git a/src/messageview.cpp b/src/messageview.cpp new file mode 100644 index 0000000..476d115 --- /dev/null +++ b/src/messageview.cpp @@ -0,0 +1,37 @@ +#include "messageview.h" + +#include "config.h" + +#include +#include +#include + +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(); + }); +} diff --git a/src/messageview.h b/src/messageview.h new file mode 100644 index 0000000..da74933 --- /dev/null +++ b/src/messageview.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +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; +}; diff --git a/src/statusbar.cpp b/src/statusbar.cpp index 7b9a4c8..4c86518 100644 --- a/src/statusbar.cpp +++ b/src/statusbar.cpp @@ -2,72 +2,146 @@ #include #include +#include +#include #include -#include + +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) : QWidget(parent) , m_mode(new QLabel(this)) - , m_url(new QLabel(this)) - , m_progress(new QLabel(this)) + , m_keys(new QLabel(this)) + , m_url(new ElidedLabel(this)) + , m_scroll(new QLabel(this)) + , m_progress(new QProgressBar(this)) { setObjectName(QStringLiteral("statusBar")); + // A plain QWidget subclass only paints a stylesheet background with this. + setAttribute(Qt::WA_StyledBackground); m_mode->setObjectName(QStringLiteral("statusMode")); + m_keys->setObjectName(QStringLiteral("statusKeys")); m_url->setObjectName(QStringLiteral("statusUrl")); + m_scroll->setObjectName(QStringLiteral("statusScroll")); m_progress->setObjectName(QStringLiteral("statusProgress")); 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); - layout->setContentsMargins(4, 1, 4, 1); + layout->setContentsMargins(4, 0, 4, 0); layout->setSpacing(8); layout->addWidget(m_mode); + layout->addWidget(m_keys); layout->addWidget(m_url, 1); + layout->addWidget(m_scroll); layout->addWidget(m_progress); + + setMode(QString()); + setScrollPercent(0); } void StatusBar::setMode(const QString &mode) { m_mode->setText(mode.isEmpty() ? QString() - : QStringLiteral("-- %1 --").arg(mode.toUpper())); - m_mode->setVisible(!mode.isEmpty()); + : QStringLiteral("-- %1 MODE --").arg(mode.toUpper())); + setStyleProperty(this, "mode", mode); +} + +void StatusBar::setKeyString(const QString &keys) +{ + m_keys->setText(keys); } void StatusBar::setUrl(const QUrl &url) { - m_urlText = url.toDisplayString(); - m_insecure = url.scheme() == QLatin1String("http") || url.scheme() == QLatin1String("ws"); - setUrlLabelState(false, m_insecure); - m_url->setText(m_urlText); + m_pageUrl = url; + updateUrl(); +} + +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) { - const bool loading = percent > 0 && percent < 100; - m_progress->setText(loading ? QStringLiteral("[%1%]").arg(percent) : QString()); + const bool loading = percent < 100; + if (loading && !m_loading) { + m_loading = true; + m_loadFailed = false; + updateUrl(); + } + m_progress->setValue(percent); m_progress->setVisible(loading); } -void StatusBar::showMessage(const QString &message, bool isError) +void StatusBar::setScrollPercent(int percent) { - setUrlLabelState(isError, false); - m_url->setText(message); - - QTimer::singleShot(4000, this, &StatusBar::clearMessage); + m_scroll->setText(percent <= 0 ? QStringLiteral("[top]") + : percent >= 100 ? QStringLiteral("[bot]") + : QStringLiteral("[%1%]").arg(percent)); } -void StatusBar::clearMessage() +void StatusBar::updateUrl() { - setUrlLabelState(false, m_insecure); - m_url->setText(m_urlText); -} - -void StatusBar::setUrlLabelState(bool error, bool insecure) -{ - m_url->setProperty("error", error); - m_url->setProperty("insecure", insecure); - // Re-polish so the stylesheet picks up the changed properties. - m_url->style()->unpolish(m_url); - m_url->style()->polish(m_url); + // qutebrowser's statusbar.url.* colours: aqua for a hovered link, orange + // after a failed load, green once an HTTPS page has loaded, white while + // loading and for plain HTTP (which never turns green). + QString state; + if (!m_hoverUrl.isEmpty()) + state = QStringLiteral("hover"); + else if (m_loadFailed) + state = QStringLiteral("error"); + else if (!m_loading && m_pageUrl.scheme() == QLatin1String("https")) + state = QStringLiteral("https"); + setStyleProperty(m_url, "state", state); + m_url->setText(m_hoverUrl.isEmpty() ? m_pageUrl.toDisplayString() : m_hoverUrl); } diff --git a/src/statusbar.h b/src/statusbar.h index 6cb1b21..815c6fb 100644 --- a/src/statusbar.h +++ b/src/statusbar.h @@ -4,9 +4,12 @@ #include class QLabel; +class QProgressBar; -// The line that replaces the command line when no command is being typed: -// current mode on the left, URL in the middle, load progress on the right. +// The line at the bottom of the window, laid out like qutebrowser's: the mode +// 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 { Q_OBJECT @@ -14,21 +17,29 @@ class StatusBar : public QWidget public: explicit StatusBar(QWidget *parent = nullptr); + // Empty for normal mode. "insert" also turns the bar green. void setMode(const QString &mode); - // Plain-HTTP URLs are shown in a warning colour: anyone on the network - // can read and change those pages. + // A pending key sequence, e.g. "g". + void setKeyString(const QString &keys); 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); - // Transient feedback: errors, unknown commands, yanked URLs. - void showMessage(const QString &message, bool isError = false); - void clearMessage(); + // 0-100, shown as [top], [bot] or [NN%]. + void setScrollPercent(int percent); private: - void setUrlLabelState(bool error, bool insecure); + void updateUrl(); QLabel *m_mode; + QLabel *m_keys; QLabel *m_url; - QLabel *m_progress; - QString m_urlText; - bool m_insecure = false; + QLabel *m_scroll; + QProgressBar *m_progress; + + QUrl m_pageUrl; + QString m_hoverUrl; + bool m_loading = false; + bool m_loadFailed = false; };