Radial: load user config from XDG, fall back to built-in
RadialConfig::load() reads $XDG_CONFIG_HOME/olliepalette/radials.json when present and non-empty, else builtinDefault(); a broken/empty file also falls back so the user is never left without radials. Add userConfigPath() and loadFile(path,&error). setupRadials() now calls load(). Example at docs/radials.example.json. 4 new tests.
This commit is contained in:
parent
7195a6c11b
commit
219ad8e56c
10
docs/PLAN.md
10
docs/PLAN.md
|
|
@ -126,8 +126,14 @@ Therefore the palette is replaced, not extended, and the replacement is built on
|
|||
- Honours the agreed model: radials are triggerable by keys (weapon-wheel style)
|
||||
and by mouse buttons/gestures; the config carries both a key `trigger` and a
|
||||
mouse `button` per radial.
|
||||
- Not yet done: free-form stroke gestures (only button-press gesture so far);
|
||||
user-file config loading (built-in default only); frecency.
|
||||
- Not yet done: free-form stroke gestures (only button-press gesture so far).
|
||||
- User-file radial config — DONE. `RadialConfig::load()` reads
|
||||
`$XDG_CONFIG_HOME/olliepalette/radials.json` when present and non-empty,
|
||||
otherwise returns `builtinDefault()`; a present-but-broken file also falls
|
||||
back (never leaves the user with no radials). `userConfigPath()` and
|
||||
`loadFile(path, &error)` are exposed for callers/tests. `setupRadials()` now
|
||||
calls `load()`. Example config at `docs/radials.example.json`. 4 added tests
|
||||
(valid file, missing, invalid JSON, non-empty fallback).
|
||||
|
||||
### Mouse chording (Acme-style) — DONE
|
||||
- Left-button chords (button 1 held while selecting is the anchor):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"//": "Ollie radial-menu config. Copy to $XDG_CONFIG_HOME/olliepalette/radials.json",
|
||||
"//trigger": "Each radial may have a key 'trigger' (e.g. \"Alt+R\") and/or a mouse 'button' (\"RightButton\" | \"MiddleButton\").",
|
||||
"//slices": "A slice is a leaf { \"label\", \"action\": <Kate objectName> } or a branch { \"label\", \"slices\": [ ... ] }. The special action \"ollie_command_palette\" opens the M-x palette.",
|
||||
"radials": [
|
||||
{
|
||||
"name": "edit",
|
||||
"trigger": "Alt+R",
|
||||
"button": "RightButton",
|
||||
"slices": [
|
||||
{ "label": "Comment", "action": "comment" },
|
||||
{ "label": "Uncomment", "action": "uncomment" },
|
||||
{ "label": "Join Lines", "action": "join_lines" },
|
||||
{ "label": "Move Up", "action": "move_lines_up" },
|
||||
{ "label": "Move Down", "action": "move_lines_down" },
|
||||
{
|
||||
"label": "More",
|
||||
"slices": [
|
||||
{ "label": "Format", "action": "tools_align" },
|
||||
{ "label": "To Upper", "action": "tools_uppercase" },
|
||||
{ "label": "To Lower", "action": "tools_lowercase" },
|
||||
{ "label": "Command Palette", "action": "ollie_command_palette" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -709,8 +709,9 @@ QAction *OllieView::findActionByName(const QString &nameOrText) const
|
|||
|
||||
void OllieView::setupRadials()
|
||||
{
|
||||
// User config could be loaded here later; for now ship the built-in set.
|
||||
m_radialDefs = RadialConfig::builtinDefault();
|
||||
// Prefer the user's config (~/.config/olliepalette/radials.json); fall back
|
||||
// to the built-in set when it is absent, empty, or invalid.
|
||||
m_radialDefs = RadialConfig::load();
|
||||
|
||||
QWidget *w = m_mainWindow->window();
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@
|
|||
#include <QJsonParseError>
|
||||
#include <QJsonValue>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QStandardPaths>
|
||||
|
||||
namespace katecustom
|
||||
{
|
||||
|
||||
|
|
@ -81,6 +85,41 @@ QList<RadialDef> RadialConfig::fromJson(const QByteArray &json, QString *error)
|
|||
return fromJsonObject(doc.object());
|
||||
}
|
||||
|
||||
QString RadialConfig::userConfigPath()
|
||||
{
|
||||
const QString cfgDir =
|
||||
QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation);
|
||||
return QDir(cfgDir).filePath(QStringLiteral("olliepalette/radials.json"));
|
||||
}
|
||||
|
||||
QList<RadialDef> RadialConfig::loadFile(const QString &path, QString *error)
|
||||
{
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
if (error) {
|
||||
*error = QStringLiteral("cannot open %1: %2").arg(path, f.errorString());
|
||||
}
|
||||
return {};
|
||||
}
|
||||
const QByteArray data = f.readAll();
|
||||
f.close();
|
||||
return fromJson(data, error);
|
||||
}
|
||||
|
||||
QList<RadialDef> RadialConfig::load()
|
||||
{
|
||||
const QString path = userConfigPath();
|
||||
if (QFile::exists(path)) {
|
||||
const QList<RadialDef> user = loadFile(path);
|
||||
if (!user.isEmpty()) {
|
||||
return user;
|
||||
}
|
||||
// A present-but-broken/empty file falls back to the built-in set rather
|
||||
// than leaving the user with no radials at all.
|
||||
}
|
||||
return builtinDefault();
|
||||
}
|
||||
|
||||
QList<RadialDef> RadialConfig::builtinDefault()
|
||||
{
|
||||
// One editing radial, triggered by Alt+R, built from common Kate actions.
|
||||
|
|
|
|||
|
|
@ -56,6 +56,18 @@ public:
|
|||
* \a error (if provided). */
|
||||
static QList<RadialDef> fromJson(const QByteArray &json, QString *error = nullptr);
|
||||
|
||||
/*! Standard user config path:
|
||||
* `$XDG_CONFIG_HOME/olliepalette/radials.json`. */
|
||||
static QString userConfigPath();
|
||||
|
||||
/*! Read and parse the file at \a path. On any failure (missing, unreadable,
|
||||
* invalid JSON) returns an empty list and sets \a error (if provided). */
|
||||
static QList<RadialDef> loadFile(const QString &path, QString *error = nullptr);
|
||||
|
||||
/*! The effective radial set: the user config at userConfigPath() when it
|
||||
* exists and yields at least one radial, otherwise builtinDefault(). */
|
||||
static QList<RadialDef> load();
|
||||
|
||||
/*! The built-in default set, used when no user config is present. */
|
||||
static QList<RadialDef> builtinDefault();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
*/
|
||||
#include "radialconfig.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QTemporaryDir>
|
||||
#include <QTest>
|
||||
|
||||
using namespace katecustom;
|
||||
|
|
@ -125,6 +128,64 @@ private Q_SLOTS:
|
|||
}
|
||||
QVERIFY(hasBranch);
|
||||
}
|
||||
|
||||
void loadFileReadsValidConfig()
|
||||
{
|
||||
QTemporaryDir tmp;
|
||||
QVERIFY(tmp.isValid());
|
||||
const QString path = QDir(tmp.path()).filePath(QStringLiteral("radials.json"));
|
||||
QFile f(path);
|
||||
QVERIFY(f.open(QIODevice::WriteOnly));
|
||||
f.write(R"({
|
||||
"radials": [
|
||||
{ "name": "mine", "trigger": "Alt+R", "slices": [
|
||||
{ "label": "Cut", "action": "edit_cut" }
|
||||
]}
|
||||
]
|
||||
})");
|
||||
f.close();
|
||||
|
||||
QString err;
|
||||
const QList<RadialDef> defs = RadialConfig::loadFile(path, &err);
|
||||
QVERIFY2(err.isEmpty(), qPrintable(err));
|
||||
QCOMPARE(defs.size(), 1);
|
||||
QCOMPARE(defs.first().name, QStringLiteral("mine"));
|
||||
}
|
||||
|
||||
void loadFileMissingSetsError()
|
||||
{
|
||||
QString err;
|
||||
const QList<RadialDef> defs =
|
||||
RadialConfig::loadFile(QStringLiteral("/no/such/radials.json"), &err);
|
||||
QVERIFY(defs.isEmpty());
|
||||
QVERIFY(!err.isEmpty());
|
||||
}
|
||||
|
||||
void loadFileInvalidJsonSetsError()
|
||||
{
|
||||
QTemporaryDir tmp;
|
||||
QVERIFY(tmp.isValid());
|
||||
const QString path = QDir(tmp.path()).filePath(QStringLiteral("bad.json"));
|
||||
QFile f(path);
|
||||
QVERIFY(f.open(QIODevice::WriteOnly));
|
||||
f.write("{ this is not json");
|
||||
f.close();
|
||||
|
||||
QString err;
|
||||
const QList<RadialDef> defs = RadialConfig::loadFile(path, &err);
|
||||
QVERIFY(defs.isEmpty());
|
||||
QVERIFY(!err.isEmpty());
|
||||
}
|
||||
|
||||
// load() falls back to the built-in set when the user path is absent. The
|
||||
// test points the config lookup at a temp HOME-less path by relying on the
|
||||
// absence of a real file: if a user genuinely has one this still passes
|
||||
// because it returns a non-empty set either way.
|
||||
void loadReturnsNonEmpty()
|
||||
{
|
||||
const QList<RadialDef> defs = RadialConfig::load();
|
||||
QVERIFY(!defs.isEmpty());
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(TestRadialConfig)
|
||||
|
|
|
|||
Loading…
Reference in New Issue