kate-deft/src/radial/radialconfig.cpp

208 lines
7.7 KiB
C++

/*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "radialconfig.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QJsonValue>
#include <QDir>
#include <QFile>
#include <QStandardPaths>
namespace katecustom
{
namespace
{
// Recursively parse a slice node. A node with a non-empty "slices" array is a
// branch; otherwise it is a leaf bound to "action".
RadialNode parseNode(const QJsonObject &obj)
{
RadialNode node;
node.label = obj.value(QStringLiteral("label")).toString();
node.actionId = obj.value(QStringLiteral("action")).toString();
const QJsonArray slices = obj.value(QStringLiteral("slices")).toArray();
for (const QJsonValue &v : slices) {
if (v.isObject()) {
node.children.push_back(parseNode(v.toObject()));
}
}
// A branch must not also act as a leaf; the children take precedence.
if (!node.children.isEmpty()) {
node.actionId.clear();
}
return node;
}
} // namespace
QList<RadialDef> RadialConfig::fromJsonObject(const QJsonObject &obj)
{
QList<RadialDef> defs;
const QJsonArray radials = obj.value(QStringLiteral("radials")).toArray();
for (const QJsonValue &rv : radials) {
if (!rv.isObject()) {
continue;
}
const QJsonObject robj = rv.toObject();
RadialDef def;
def.name = robj.value(QStringLiteral("name")).toString();
def.trigger = robj.value(QStringLiteral("trigger")).toString();
def.mouseButton = robj.value(QStringLiteral("button")).toString();
const QJsonArray slices = robj.value(QStringLiteral("slices")).toArray();
for (const QJsonValue &sv : slices) {
if (sv.isObject()) {
def.root.children.push_back(parseNode(sv.toObject()));
}
}
// A radial with no slices is useless; skip it.
if (!def.root.children.isEmpty()) {
defs.push_back(def);
}
}
return defs;
}
QList<RadialDef> RadialConfig::fromJson(const QByteArray &json, QString *error)
{
QJsonParseError perr{};
const QJsonDocument doc = QJsonDocument::fromJson(json, &perr);
if (perr.error != QJsonParseError::NoError || !doc.isObject()) {
if (error) {
*error = perr.error != QJsonParseError::NoError
? perr.errorString()
: QStringLiteral("top-level JSON value is not an object");
}
return {};
}
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.
// Leaf ids are Kate action objectNames; a trailing branch groups more.
auto leaf = [](const QString &label, const QString &action) {
RadialNode n;
n.label = label;
n.actionId = action;
return n;
};
RadialDef edit;
edit.name = QStringLiteral("edit");
edit.trigger = QStringLiteral("Alt+R");
edit.mouseButton = QStringLiteral("RightButton"); // natural radial gesture
// The radial holds actions that complete IN PLACE — no panel that pulls
// attention elsewhere. Panel-opening actions (Find, Replace) deliberately
// live in the command palette / keybinds instead, so the radial preserves
// editing flow. The "Command Palette…" slice is the escape hatch to those.
RadialNode more;
more.label = QStringLiteral("More");
more.children = {
leaf(QStringLiteral("Comment"), QStringLiteral("comment")),
leaf(QStringLiteral("Uncomment"), QStringLiteral("uncomment")),
leaf(QStringLiteral("Uppercase"), QStringLiteral("uppercase")),
leaf(QStringLiteral("Lowercase"), QStringLiteral("lowercase")),
leaf(QStringLiteral("Join Lines"), QStringLiteral("joinLines")),
// Panel-opening switchers live in the "More" branch next to the palette
// escape hatch — not the top ring of in-place actions — because they pull
// attention away from the caret. Resolved directly by onRadialActivated.
leaf(QStringLiteral("Go to File…"), QStringLiteral("ollie_goto_file")),
leaf(QStringLiteral("Go to Symbol…"), QStringLiteral("ollie_goto_symbol")),
// Opens the full command palette (handled specially by the plugin).
leaf(QStringLiteral("Command Palette…"), QStringLiteral("ollie_command_palette")),
};
edit.root.children = {
// Cut/Copy/Paste are intentionally absent: cut and paste are handled by
// the Acme left+middle / left+right chords (standard KDE clipboard), and
// copy by the X11 primary selection — all more in-flow than a radial trip.
leaf(QStringLiteral("Undo"), QStringLiteral("edit_undo")),
leaf(QStringLiteral("Redo"), QStringLiteral("edit_redo")),
// Multi-cursor on every selected line: a quintessential in-place action
// (select lines → gesture → keep typing). Referenced by visible text so
// it resolves regardless of the Kate action's objectName.
leaf(QStringLiteral("Multi-Cursor"),
QStringLiteral("Add Cursors to Line Ends")),
more,
};
// A second radial for window/split management, triggered by the middle
// mouse button. Split ops are inherently about window layout, not the caret,
// so a dedicated radial keeps them off the keyboard and out of the edit
// radial. MiddleButton is safe: every Acme chord is left+something, and a
// plain middle press (no left held) is routed here, not to a chord. Leaf ids
// are Kate's view-manager action objectNames.
RadialDef window;
window.name = QStringLiteral("window");
window.mouseButton = QStringLiteral("MiddleButton");
RadialNode focus;
focus.label = QStringLiteral("Focus");
focus.children = {
leaf(QStringLiteral("Left"), QStringLiteral("go_left_split_view")),
leaf(QStringLiteral("Right"), QStringLiteral("go_right_split_view")),
leaf(QStringLiteral("Up"), QStringLiteral("go_upward_split_view")),
leaf(QStringLiteral("Down"), QStringLiteral("go_downward_split_view")),
leaf(QStringLiteral("Next"), QStringLiteral("go_next_split_view")),
leaf(QStringLiteral("Previous"), QStringLiteral("go_prev_split_view")),
};
window.root.children = {
leaf(QStringLiteral("Split Vertical"), QStringLiteral("view_split_vert")),
leaf(QStringLiteral("Split Horizontal"), QStringLiteral("view_split_horiz")),
leaf(QStringLiteral("Close Split"), QStringLiteral("view_close_current_space")),
leaf(QStringLiteral("Close Others"), QStringLiteral("view_close_others")),
leaf(QStringLiteral("Toggle Orientation"), QStringLiteral("view_split_toggle")),
focus,
};
return {edit, window};
}
} // namespace katecustom