/* * SPDX-License-Identifier: LGPL-2.0-or-later */ #include "radialconfig.h" #include #include #include #include #include #include #include #include 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 RadialConfig::fromJsonObject(const QJsonObject &obj) { QList 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 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 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 RadialConfig::load() { const QString path = userConfigPath(); if (QFile::exists(path)) { const QList 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 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