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.
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.
+2
View File
@@ -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
+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
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=<hex or base64>` or `…#sha256-<base64>`),
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 |
+67 -26
View File
@@ -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<QWidget *>(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()
+4
View File
@@ -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;
+1
View File
@@ -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);
+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,
};
// 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;
+8 -4
View File
@@ -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)
+55 -22
View File
@@ -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))
+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 <QLabel>
#include <QPainter>
#include <QProgressBar>
#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)
: 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);
}
+22 -11
View File
@@ -4,9 +4,12 @@
#include <QWidget>
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;
};