208 lines
7.7 KiB
C++
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
|