make bar look like the one from qutebrowser
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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))
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user