v0.8.14: icons from the desktop's theme (Breeze), readable text, composer frame
The window drew its icons with unicode glyphs -- a clip emoji for attach, a
gear, a hamburger, a chevron, a multiplication sign for delete. Those render
in whichever font happens to cover them (colour emoji in a grey toolbar) and
are a box on a machine without that glyph. They now come from the icon theme,
the way every other desktop app gets them.
Icons
- app/src/ui/ThemeIcons.{h,cpp}: a QQuickImageProvider for
image://csttheme/<name>?color=%23rrggbb -- QIcon::fromTheme() in the
desktop's theme (whatever the platform theme set, else breeze-dark, breeze,
hicolor), tinted with QPainter's SourceIn and rasterised at the requested
size and the screen's device pixel ratio. Qt only builds its theme search
paths from XDG once a theme name is set, so the provider fills them itself
(GenericDataLocation/icons, ~/.icons and, on NixOS,
/run/current-system/sw/share/icons, for a shell started outside the desktop
session). An unknown name yields a null image, which QML reports as
Image.Error.
- app/qml/ThemeIcon.qml: the one component that asks for an icon. Every use
still names the old character as `glyph:`, so a host with no icon theme
degrades to exactly what the window looked like before instead of going
blank. installThemeIcons() is called by main() and by --uitest, so the test
loads the same icons the app does.
- Replaced: nav (application-menu), settings (settings-configure), attach
(mail-attachment), copy (edit-copy), session delete (edit-delete), new
session (list-add), expand/collapse (arrow-right/arrow-down), jump to the end
(go-down), the plan checklist (task-complete/box). A tool card also shows
what kind of tool ran -- Theme.toolIcon(): pencil for patch/write, text page
for read, folder for list/glob, magnifier for grep, braces for a shell,
globe for web, list for a plan. Toolbar icons are 22 px (Breeze's toolbar
size), card and row icons 14-16 px.
Colours (the "some text is unreadable" part)
- textFaint #6d757b -> #8d959c: ~3.4:1 -> ~5:1 on the card fill. It carries
hints, provider URLs, session metadata and placeholders, so it is text, not
decoration.
- A selected provider row and the open session row write on the accent fill;
they now use accentText / accentTextDim / okOnAccent instead of faint grey
and green on blue (named "accentText", not "onAccent": QML reads any `onFoo`
property as a signal handler and refuses the assignment).
- The filled buttons (Send, Use this) take their ink from Theme.accentInk.
Composer
- The frame is one Theme.pad around its column on every side (anchors.fill,
implicitHeight + 2*pad). With the top margin alone the last row -- the clip
and Send -- hung over the lower border, which is what put the clip on the
frame instead of inside it.
app/README.md gains an "Icons, colors" section (the provider, why the theme
search paths are filled by hand, the qtsvg requirement for rasterising Breeze,
the tool map, and the contrast rules).
Verified: --uitest 52/52, --selftest OK, and the 0.8.14 window relaunched in
the Plasma session (no QML warnings).
Version 0.8.14.
mode = coding
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
#include "core/AgentCore.h"
|
||||
#include "core/ToolProvider.h"
|
||||
#include "ui/UiTest.h"
|
||||
#include "ui/ThemeIcons.h"
|
||||
#include "cst_version.h"
|
||||
|
||||
namespace {
|
||||
@@ -192,6 +193,8 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
/* image://csttheme/ plus the desktop's icon theme behind it. */
|
||||
app::installThemeIcons(&engine);
|
||||
engine.rootContext()->setContextProperty(QStringLiteral("agent"), &core);
|
||||
engine.rootContext()->setContextProperty(QStringLiteral("initialWidth"), winW);
|
||||
engine.rootContext()->setContextProperty(QStringLiteral("initialHeight"), winH);
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
#include "ui/ThemeIcons.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QGuiApplication>
|
||||
#include <QIcon>
|
||||
#include <QPainter>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QScreen>
|
||||
#include <QStandardPaths>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
|
||||
namespace app {
|
||||
namespace {
|
||||
|
||||
/* What a usable theme has to have; every window in the app needs it. */
|
||||
const char kProbe[] = "mail-attachment";
|
||||
|
||||
/* The desktop's own theme first (whatever the platform theme set), then
|
||||
* Breeze: this window is dark, so the dark variant, and Breeze proper as
|
||||
* Qt's fallback for the odd name that only lives there. Before any of that
|
||||
* the search paths have to be filled in -- Qt only builds them from XDG once
|
||||
* a theme name was set, and a shell that was started outside the desktop
|
||||
* session (nix-shell) has no XDG_DATA_DIRS pointing at the icon packages,
|
||||
* hence the system profile. */
|
||||
void adoptIconTheme() {
|
||||
const QStringList known = QStandardPaths::standardLocations(
|
||||
QStandardPaths::GenericDataLocation);
|
||||
QStringList paths;
|
||||
for (const QString &d : known)
|
||||
paths << d + QStringLiteral("/icons");
|
||||
paths << QDir::homePath() + QStringLiteral("/.icons");
|
||||
const QString system = QStringLiteral("/run/current-system/sw/share/icons");
|
||||
if (!paths.contains(system) && QDir(system).exists())
|
||||
paths << system;
|
||||
paths << QStringLiteral(":/icons");
|
||||
QIcon::setThemeSearchPaths(paths);
|
||||
|
||||
QStringList candidates;
|
||||
if (!QIcon::themeName().isEmpty())
|
||||
candidates << QIcon::themeName();
|
||||
candidates << QStringLiteral("breeze-dark") << QStringLiteral("breeze")
|
||||
<< QStringLiteral("hicolor");
|
||||
for (const QString &name : candidates) {
|
||||
QIcon::setThemeName(name);
|
||||
if (QIcon::hasThemeIcon(QLatin1String(kProbe)))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* "?color=%23eff0f1": the tint, and nothing else so far. */
|
||||
QColor colorOption(const QString &id, QString *name) {
|
||||
QColor color;
|
||||
*name = id;
|
||||
const int q = id.indexOf(QLatin1Char('?'));
|
||||
if (q < 0)
|
||||
return color;
|
||||
*name = id.left(q);
|
||||
const QStringList options = id.mid(q + 1).split(QLatin1Char('&'));
|
||||
for (const QString &opt : options) {
|
||||
if (!opt.startsWith(QLatin1String("color=")))
|
||||
continue;
|
||||
const QColor c(QUrl::fromPercentEncoding(opt.mid(6).toUtf8()));
|
||||
if (c.isValid())
|
||||
color = c;
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
qreal targetRatio() {
|
||||
if (const QScreen *s = QGuiApplication::primaryScreen())
|
||||
return qMax<qreal>(1.0, s->devicePixelRatio());
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ThemeIconProvider::ThemeIconProvider()
|
||||
: QQuickImageProvider(QQuickImageProvider::Image) {}
|
||||
|
||||
QImage ThemeIconProvider::requestImage(const QString &id, QSize *size,
|
||||
const QSize &requestedSize) {
|
||||
QString name;
|
||||
const QColor color = colorOption(id, &name);
|
||||
if (name.isEmpty())
|
||||
return QImage();
|
||||
|
||||
const QIcon icon = QIcon::fromTheme(name);
|
||||
if (icon.isNull())
|
||||
return QImage(); /* QML shows its fallback glyph */
|
||||
|
||||
const int px = requestedSize.isValid() && requestedSize.width() > 0
|
||||
? requestedSize.width()
|
||||
: 22;
|
||||
const qreal ratio = targetRatio();
|
||||
QPixmap pm = icon.pixmap(QSize(px, px), ratio, QIcon::Normal, QIcon::Off);
|
||||
if (pm.isNull())
|
||||
return QImage();
|
||||
if (color.isValid()) {
|
||||
QPainter p(&pm);
|
||||
p.setCompositionMode(QPainter::CompositionMode_SourceIn);
|
||||
p.fillRect(pm.rect(), color);
|
||||
}
|
||||
QImage img = pm.toImage();
|
||||
img.setDevicePixelRatio(ratio);
|
||||
if (size)
|
||||
*size = QSize(px, px);
|
||||
return img;
|
||||
}
|
||||
|
||||
void installThemeIcons(QQmlApplicationEngine *engine) {
|
||||
adoptIconTheme();
|
||||
if (engine)
|
||||
engine->addImageProvider(QStringLiteral("csttheme"), new ThemeIconProvider);
|
||||
}
|
||||
|
||||
QString iconThemeName() { return QIcon::themeName(); }
|
||||
|
||||
bool themeIconAvailable(const QString &name) {
|
||||
return QIcon::hasThemeIcon(name);
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
@@ -0,0 +1,47 @@
|
||||
/* Icons from the desktop's icon theme (Breeze on KDE).
|
||||
*
|
||||
* The window did its icons with unicode glyphs (a clip emoji for attach, a
|
||||
* gear, a hamburger): they render in whatever font happens to cover them --
|
||||
* colour emoji in a grey toolbar -- and on a machine without that glyph they
|
||||
* are a box. Every other desktop app asks the icon theme instead, and Qt
|
||||
* Quick has no image provider for it without Kirigami, so this is one:
|
||||
*
|
||||
* image://csttheme/<icon-name>?color=%23rrggbb
|
||||
*
|
||||
* The icon is looked up with QIcon::fromTheme() in the desktop's theme
|
||||
* (breeze-dark on a KDE desktop, Breeze as fallback), tinted to the asked-for
|
||||
* color and rendered at the requested size. An unknown name yields a null
|
||||
* image, which QML sees as Image.Error -- that is the hook for a glyph
|
||||
* fallback, used on a machine with no icon theme at all. */
|
||||
|
||||
#ifndef CST_APP_THEMEICONS_H
|
||||
#define CST_APP_THEMEICONS_H
|
||||
|
||||
#include <QImage>
|
||||
#include <QQuickImageProvider>
|
||||
#include <QString>
|
||||
|
||||
class QQmlApplicationEngine;
|
||||
|
||||
namespace app {
|
||||
|
||||
class ThemeIconProvider : public QQuickImageProvider {
|
||||
public:
|
||||
ThemeIconProvider();
|
||||
QImage requestImage(const QString &id, QSize *size,
|
||||
const QSize &requestedSize) override;
|
||||
};
|
||||
|
||||
/* Points Qt at the desktop's icon theme and registers image://csttheme/.
|
||||
* Call it on every engine that loads the window (main.cpp and --uitest). */
|
||||
void installThemeIcons(QQmlApplicationEngine *engine);
|
||||
|
||||
/* The theme Qt was left on, for --selftest and the README. */
|
||||
QString iconThemeName();
|
||||
|
||||
/* Whether the theme has this icon (what the fallback glyphs are for). */
|
||||
bool themeIconAvailable(const QString &name);
|
||||
|
||||
} // namespace app
|
||||
|
||||
#endif /* CST_APP_THEMEICONS_H */
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <cstring>
|
||||
|
||||
#include "cst_agent.h"
|
||||
#include "ui/ThemeIcons.h"
|
||||
|
||||
namespace app {
|
||||
namespace {
|
||||
@@ -173,6 +174,7 @@ int runUiTest(AgentCore &core, const QString &size) {
|
||||
}
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
installThemeIcons(&engine); /* the window's icons, same as main() */
|
||||
engine.rootContext()->setContextProperty(QStringLiteral("agent"), &core);
|
||||
engine.rootContext()->setContextProperty(QStringLiteral("initialWidth"), width);
|
||||
engine.rootContext()->setContextProperty(QStringLiteral("initialHeight"), height);
|
||||
|
||||
Reference in New Issue
Block a user