Home Reference Source

cables_dev/cables_ui/src/ui/commands/cmd_patch.js

import ModalDialog from "../dialogs/modaldialog.js";
import Gui from "../gui.js";
import { getHandleBarHtml } from "../utils/handlebars.js";
import { notifyError } from "../elements/notification.js";
import AnalyzePatchTab from "../components/tabs/tab_analyze.js";
import OpParampanel from "../components/opparampanel/op_parampanel.js";
import GlOpWatcher from "../components/tabs/tab_glop.js";
import ManageOp from "../components/tabs/tab_manage_op.js";
import defaultOps from "../defaultops.js";
import subPatchOpUtil from "../subpatchop_util.js";

const CABLES_CMD_PATCH = {};
const CMD_PATCH_COMMANDS = [];

const patchCommands =
{
    "commands": CMD_PATCH_COMMANDS,
    "functions": CABLES_CMD_PATCH
};

export default patchCommands;

CABLES_CMD_PATCH.setPatchTitle = () =>
{
    gui.patchView.store.showModalTitleDialog();
};

CABLES_CMD_PATCH.openParamsTab = () =>
{
    const ops = gui.patchView.getSelectedOps();
    if (!ops.length) return;

    const op = gui.patchView.getSelectedOps()[0];
    const id = "params" + CABLES.uuid();

    const tab = new CABLES.UI.Tab(op.name, { "icon": "op", "infotext": "tab_timeline", "padding": true, "singleton": false });
    gui.mainTabs.addTab(tab, true);
    gui.maintabPanel.show(true);
    tab.html("<div id=\"" + id + "\"></div<");

    const opParams = new OpParampanel();

    opParams.setParentElementId(id);
    opParams.show(op);
};

CABLES_CMD_PATCH.clearOpTitles = function ()
{
    let ops = gui.patchView.getSelectedOps();

    if (ops.length == 0)ops = gui.corePatch().ops;

    if (!ops || ops.length == 0) return;

    for (let i = 0; i < ops.length; i++)
    {
        const op = gui.corePatch().getOpById(ops[i].id);
        op.setTitle("");
    }
};

CABLES_CMD_PATCH.selectChilds = function ()
{
    const ops = gui.patchView.getSelectedOps();

    if (!ops || ops.length == 0) return;

    for (let i = 0; i < ops.length; i++)
    {
        const op = gui.corePatch().getOpById(ops[i].id);
        op.selectChilds();
    }

    gui.patchView.showSelectedOpsPanel();
};

CABLES_CMD_PATCH.autoPosSubpatchInputOutputOps = function ()
{
    const sub = gui.patchView.getCurrentSubPatch();
    if (!sub) return;
    gui.patchView.setPositionSubPatchInputOutputOps(sub);
};


CABLES_CMD_PATCH.gotoParentSubpatch = function ()
{
    const names = gui.patchView.getSubpatchPathArray(gui.patchView.getCurrentSubPatch());

    if (names.length == 0) return;
    if (names.length == 1) gui.patchView.setCurrentSubPatch(0);
    else gui.patchView.setCurrentSubPatch(names[names.length - 1].id);
};

CABLES_CMD_PATCH.selectAllOps = function ()
{
    gui.patchView.selectAllOpsSubPatch(gui.patchView.getCurrentSubPatch());
};

CABLES_CMD_PATCH.deleteSelectedOps = function ()
{
    gui.patchView.deleteSelectedOps();
};

CABLES_CMD_PATCH.reload = function ()
{
    CABLESUILOADER.talkerAPI.send("reload");
};

CABLES_CMD_PATCH.editOp = function (userInteraction = true)
{
    const selops = gui.patchView.getSelectedOps();

    if (selops && selops.length > 0)
    {
        for (let i = 0; i < selops.length; i++) gui.serverOps.edit(selops[i], false, null, userInteraction);
    }
};

CABLES_CMD_PATCH.createVersionSelectedOp = function ()
{
    const ops = gui.patchView.getSelectedOps();
    if (ops.length == 0) return;

    const opname = ops[0].objName;
    let newOpname = "";
    if (opname.contains("_v"))
    {
        const parts = opname.split("_v");
        newOpname = parts[0] + "_v" + (parseFloat(parts[1]) + 1);
    }
    else newOpname = opname + "_v2";

    gui.serverOps.clone(ops[0].opId, newOpname, () =>
    {
        gui.serverOps.loadOpDependencies(opname, function ()
        {
            gui.patchView.replaceOp(ops[0].id, newOpname);

            CABLES.UI.notify("created op " + newOpname, null, { "force": true });
        });
    });
};

CABLES_CMD_PATCH.cloneSelectedOp = function ()
{
    const ops = gui.patchView.getSelectedOps();
    if (ops.length > 0) gui.serverOps.cloneDialog(ops[0].objName, ops[0]);
};

CABLES_CMD_PATCH.manageCurrentSubpatchOp = function ()
{
    const oldSubPatchId = gui.patchView.getCurrentSubPatch();
    const subOuter = gui.patchView.getSubPatchOuterOp(oldSubPatchId);

    new ManageOp(gui.mainTabs, subOuter.opId);
};

CABLES_CMD_PATCH.manageSelectedOp = function (opid)
{
    if (!opid)
    {
        const ops = gui.patchView.getSelectedOps();
        if (ops.length > 0) opid = ops[0].opId;
    }
    new ManageOp(gui.mainTabs, opid);
};

CABLES_CMD_PATCH.save = function (force, cb)
{
    if (gui.getRestriction() < Gui.RESTRICT_MODE_FULL)
    {
        notifyError("Not allowed");
        return;
    }
    if (gui.jobs().hasJob("projectsave"))
    {
        console.log("already saving...");
        return;
    }

    gui.patchView.store.saveCurrentProject(cb, undefined, undefined, force);

    const ops = gui.savedState.getUnsavedPatchSubPatchOps();

    for (let i = 0; i < ops.length; i++)
    {
        const name = ops[i].op.shortName;
        subPatchOpUtil.updateSubPatchOpAttachment(ops[i].op, { "oldSubId": ops[i].subId });
    }
};

CABLES_CMD_PATCH.saveAs = function ()
{
    gui.patchView.store.saveAs();
};

CABLES_CMD_PATCH.createBackup = function ()
{
    CABLES.platform.createBackup();
};

CABLES_CMD_PATCH.clear = function ()
{
    gui.patchView.clearPatch();
};

CABLES_CMD_PATCH.createAreaFromSelection = function ()
{
    gui.patchView.createAreaFromSelection();
};

CABLES_CMD_PATCH.deleteUnusedPatchOps = function ()
{
    const opdocs = gui.opDocs.getAll();
    let text = "";
    let ids = [];

    for (let i = 0; i < opdocs.length; i++)
    {
        if (opdocs[i].name.indexOf("Ops.Patch") == 0)
        {
            const usedOps = gui.corePatch().getOpsByOpId(opdocs[i].id);

            if (ids.indexOf(opdocs[i].id) == -1 && usedOps.length == 0)
            {
                text += "- " + opdocs[i].name + "<br/>";
                ids.push(opdocs[i].id);
                console.log("found patch op", opdocs[i].id);
            }
        }
    }

    if (ids.length === 0)
    {
        new ModalDialog({ "title": "Unused Patch Ops", "text": "No unused patch ops found.", "showOkButton": true });
    }
    else
    {
        // this will open an iframe tab an listen to "opsDeleted" that is sent by the iframe
        const idsParam = ids.join(",");
        const url = CABLES.platform.getCablesUrl() + "/op/delete?ids=" + idsParam + "&iframe=true";
        gui.mainTabs.addIframeTab("Delete Ops", url, { "icon": "ops", "closable": true, "singleton": true, "gotoUrl": CABLES.platform.getCablesUrl() + "/op/delete?ids=" + idsParam }, true);
    }
};

CABLES_CMD_PATCH.createSubPatchOp = function ()
{
    if (!gui.project().allowEdit && gui.patchView.getCurrentSubPatch() == 0)
    {
        new ModalDialog({ "title": "You don't have write access", "showOkButton": true });
        return;
    }

    let suggestedNamespace = defaultOps.getPatchOpsNamespace();
    if (gui.patchView.getCurrentSubPatch() != 0)
    {
        const subOuter = gui.patchView.getSubPatchOuterOp(gui.patchView.getCurrentSubPatch());
        if (subOuter)
        {
            if (!gui.opDocs.getOpDocByName(subOuter.objName).allowEdit)
            {
                new ModalDialog({ "title": "You don't have write access in this subPatchOp", "showOkButton": true });
                return;
            }


            const parts = subOuter.objName.split(".");

            if (parts.length > 1)
                suggestedNamespace = parts[0] + "." + parts[1] + "." + parts[2] + ".";
        }
    }

    const dialogOptions = {
        "title": "Create operator",
        "shortName": subPatchOpUtil.getAutoName(true),
        "type": "patch",
        "suggestedNamespace": suggestedNamespace,
        "showReplace": false,
        "hasOpDirectories": CABLES.platform.frontendOptions.hasOpDirectories
    };

    if (gui.patchView.getCurrentSubPatch() != 0)
    {
        const outerOp = gui.patchView.getSubPatchOuterOp(gui.patchView.getCurrentSubPatch());
        if (outerOp)
        {
            const containerName = outerOp.objName;
            dialogOptions.sourceOpName = containerName;
        }
    }

    gui.serverOps.opNameDialog(dialogOptions, (newNamespace, newName, options) =>
    {
        gui.closeModal();
        CABLES_CMD_PATCH.createOpFromSelection({ "newOpName": newName, "ignoreNsCheck": true });
    });
};


CABLES_CMD_PATCH.centerOpsInSubpatch = function ()
{
    gui.patchView.centerSubPatchBounds(gui.patchView.getCurrentSubPatch());
};

CABLES_CMD_PATCH.createOpFromSelection = function (options = {})
{
    if (!options.ignoreNsCheck)
    {
        if (gui.patchView.getCurrentSubPatch() != 0)
        {
            const subOuter = gui.patchView.getSubPatchOuterOp(gui.patchView.getCurrentSubPatch());
            if (subOuter && subOuter.objName.indexOf("Ops.Patch.") != 0)
            {
                CABLES_CMD_PATCH.createSubPatchOp();
                return;
            }
        }
    }


    const origOpsBounds = gui.patchView.getSelectionBounds();
    gui.patchView.patchRenderer.subPatchOpAnimStart(origOpsBounds, () =>
    {
        const selops = gui.patchView.getSelectedOps();

        let selectedOpIds = gui.patchView.getSelectedOpsIds();
        const newOpname = options.newOpName || subPatchOpUtil.getAutoName();
        const currentSubpatch = gui.patchView.getCurrentSubPatch();
        // const loadingModal = gui.startModalLoading("Create Subpatch");

        for (let i = 0; i < selops.length; i++)
        {
            if (selops[i].isSubPatchOp())
            {
                if (selops[i].storage && selops[i].storage.subPatchVer != 2)
                {
                    new ModalDialog({ "title": "Can not create subPatchOp", "text": "not possible To create a subpatch op containing old subpatches. ", "showOkButton": true });

                    return;
                }
            }
        }

        gui.serverOps.create(newOpname, () =>
        {
            let newselectedOpIds = [];

            for (let i = 0; i < selectedOpIds.length; i++)
            {
                gui.patchView.selectOpId(selectedOpIds[i]);

                const op = gui.corePatch().getOpById(selectedOpIds[i]);
                if (op.isSubPatchOp())
                {
                    const newops = gui.corePatch().getSubPatchOps(op.patchId.get(), false);
                    for (let j = 0; j < newops.length; j++)
                        newselectedOpIds.push(newops[j].id);
                }
            }


            gui.patchView.createSubPatchFromSelection(2,
                (patchId, OpTempSubpatch) =>
                {
                    const portJson = { "ports": [] };
                    const oldLinks = [];

                    // find ops that are crosslinked...
                    const ops = gui.corePatch().getSubPatchOps(patchId);

                    let unlink = [];
                    for (let i = 0; i < ops.length; i++)
                    {
                        const op = ops[i];
                        for (let j = 0; j < op.portsIn.length; j++)
                        {
                            const portIn = op.portsIn[j];
                            let pJson;
                            for (let k = 0; k < op.portsIn[j].links.length; k++)
                            {
                                if (portIn.isLinked() && portIn.links[k])
                                {
                                    const p2 = portIn.links[k].getOtherPort(portIn);
                                    if (p2.op.uiAttribs.subPatch != op.uiAttribs.subPatch)
                                    {
                                        if (k == 0)
                                        {
                                            pJson = subPatchOpUtil.createBlueprintPortJsonElement(portIn);
                                            portJson.ports.push(pJson);
                                        }

                                        op.setUiAttrib({ "tempSubOldOpId": op.id });
                                        oldLinks.push({ "pJson": pJson, "port": p2, "tempSubOldOpId": op.id, "origPortName": portIn.name });
                                        unlink.push(portIn.links[k]);
                                    }
                                }
                            }
                        }

                        for (let j = 0; j < op.portsOut.length; j++)
                        {
                            const portOut = op.portsOut[j];
                            if (portOut.isLinked())
                            {
                                let pJson = null;
                                for (let k = 0; k < portOut.links.length; k++)
                                {
                                    const p2 = portOut.links[k].getOtherPort(portOut);
                                    if (p2.op.uiAttribs.subPatch != op.uiAttribs.subPatch)
                                    {
                                        if (k == 0)
                                        {
                                            pJson = subPatchOpUtil.createBlueprintPortJsonElement(portOut);
                                            portJson.ports.push(pJson);
                                        }
                                        op.setUiAttrib({ "tempSubOldOpId": op.id });
                                        oldLinks.push({ "pJson": pJson, "port": p2, "tempSubOldOpId": op.id, "origPortName": portOut.name });
                                        unlink.push(portOut.links[k]);
                                    }
                                }
                            }
                        }
                    }

                    unlink.forEach((l) => { l.remove(); });

                    gui.patchView.addOp(newOpname,
                        {
                            "uiAttribs": {
                                "translate": { "x": origOpsBounds.minX, "y": origOpsBounds.minY }
                            },
                            "onOpAdd": (newOp) =>
                            {
                                subPatchOpUtil.createBlueprint2Op(newOp, OpTempSubpatch, () =>
                                {
                                    const src = subPatchOpUtil.generatePortsAttachmentJsSrc(portJson);

                                    gui.corePatch().deleteOp(OpTempSubpatch.id);
                                    gui.patchView.setCurrentSubPatch(currentSubpatch);

                                    CABLESUILOADER.talkerAPI.send("opUpdate",
                                        {
                                            "opname": newOpname,
                                            "update": {
                                                "attachments":
                                                {
                                                    "att_inc_gen_ports.js": src,
                                                    "att_ports.json": JSON.stringify(portJson)
                                                }
                                            }
                                        },
                                        (err, r) =>
                                        {
                                            if (err)
                                            {
                                                // new ModalError({ "title": "opAttachmentSave2 Error/Invalid response from server", "text": "<pre>" + JSON.stringify(err, false, 4) + "</pre>" });
                                                this.showApiError(err);
                                                return;
                                            }

                                            gui.serverOps.execute(newOpname, (newOps) =>
                                            {
                                                newOp = newOps[0];

                                                const subPatchId = newOp.patchId.get();

                                                // relink inside ports....
                                                const subOps = gui.corePatch().getSubPatchOps(subPatchId, false);
                                                for (let j = 0; j < oldLinks.length; j++)
                                                {
                                                    // outer linking
                                                    const oldLink = oldLinks[j];
                                                    newOp.patch.link(newOp, oldLink.pJson.id, oldLink.port.op, oldLink.port.name);

                                                    for (let i = 0; i < subOps.length; i++)
                                                    {
                                                        const op = subOps[i];
                                                        if (op.uiAttribs.tempSubOldOpId == oldLink.tempSubOldOpId)
                                                        {
                                                            let patchInputOP = gui.corePatch().getSubPatch2InnerInputOp(subPatchId);
                                                            let l = newOp.patch.link(patchInputOP, "innerOut_" + oldLink.pJson.id, op, oldLink.origPortName);

                                                            if (!l)
                                                            {
                                                                let patchOutputOP = gui.corePatch().getSubPatch2InnerOutputOp(subPatchId);
                                                                l = newOp.patch.link(patchOutputOP, "innerIn_" + oldLink.pJson.id, op, oldLink.origPortName);
                                                            }

                                                            if (!l)console.log("could not recreate oldlink", oldLink);
                                                        }
                                                    }
                                                }

                                                for (let i = 0; i < subOps.length; i++) subOps[i].setUiAttrib({ "tempSubOldOpId": null });

                                                if (selectedOpIds.length == 0) newOp.setPos(0, 0);
                                                else newOp.setPos(origOpsBounds.minX, origOpsBounds.minY);

                                                gui.patchView.testCollision(newOp);
                                                gui.patchView.setPositionSubPatchInputOutputOps(subPatchId);

                                                if (!gui.savedState.getStateBlueprint(subPatchId))
                                                {
                                                    console.log("need so save subpatchop AGAIN");

                                                    subPatchOpUtil.updateSubPatchOpAttachment(newOp, { "oldSubId": subPatchId,
                                                        "next": () =>
                                                        {
                                                            // console.log("bp", bp);
                                                            // CABLES.CMD.PATCH.save();
                                                        } });
                                                }


                                                gui.patchView.patchRenderer.focusOpAnim(newOp.id);
                                                gui.patchView.patchRenderer.subPatchOpAnimEnd(newOp.id);
                                            });
                                        });
                                }, { "doNotExecute": true });
                            }
                        });
                },
                { "translate": { "x": 0, "y": 0 } });
        }, false, { "noLoadOp": true });
    });
};

// CABLES_CMD_PATCH.createSubPatchFromSelection = function (version)
// {
//     gui.patchView.createSubPatchFromSelection(version);
// };

CABLES_CMD_PATCH.findCommentedOps = function ()
{
    gui.find(":commented");
};

CABLES_CMD_PATCH.findUnconnectedOps = function ()
{
    gui.find(":unconnected");
};

CABLES_CMD_PATCH.findUserOps = function ()
{
    gui.find(":user");
};

CABLES_CMD_PATCH.findOpsUsingExternalAssets = function ()
{
    gui.find(":extassets");
};

CABLES_CMD_PATCH.createFile = function ()
{
    gui.showFileManager(function ()
    {
        gui.fileManager.createFile();
    });
};

CABLES_CMD_PATCH.uploadFile = function ()
{
    if (!window.gui) return;
    const fileElem = document.getElementById("hiddenfileElem");
    if (fileElem) fileElem.click();
};

CABLES_CMD_PATCH.reuploadFile = function (id, fileName)
{
    if (!window.gui || !fileName) return;
    CABLES.reuploadName = fileName;
    const fileEle = ele.byId("fileReUpload" + id);
    if (fileEle && fileEle.dataset.filePath) CABLES.reuploadName = fileEle.dataset.filePath;
    const uploadEle = ele.byId("hiddenfileElemReupload");
    if (uploadEle) uploadEle.click();
};


CABLES_CMD_PATCH.uploadFileDialog = function ()
{
    if (!window.gui || !gui.project()) return;
    const fileElem = document.getElementById("uploaddialog");

    if (!fileElem)
    {
        const html = getHandleBarHtml("dialog_upload", { "patchId": gui.project()._id });

        new ModalDialog({ "html": html });
    }
};

CABLES_CMD_PATCH.uploadFileTab = () =>
{
    const url = CABLES.platform.getCablesUrl() + "/patch/" + gui.project()._id + "/settings/upload?iframe=true";
    gui.mainTabs.addIframeTab(
        "Upload File",
        url,
        {
            "icon": "settings",
            "closable": true,
            "singleton": true,
        },
        true);
};

CABLES_CMD_PATCH.showBackups = () =>
{
    const url = CABLES.platform.getCablesUrl() + "/patch/" + gui.project()._id + "/settings?iframe=true#versions";
    const gotoUrl = CABLES.platform.getCablesUrl() + "/patch/" + gui.project()._id + "/settings#versions";
    gui.mainTabs.addIframeTab(
        "Patch Backups",
        url,
        {
            "icon": "settings",
            "closable": true,
            "singleton": true,
            "gotoUrl": gotoUrl
        },
        true);
};

CABLES_CMD_PATCH.export = function (type)
{
    const exporter = new CABLES.UI.Exporter(gui.project(), CABLES.platform.getPatchVersion(), type);
    exporter.show();
};

CABLES_CMD_PATCH.newPatch = function ()
{
    gui.createProject();
};

CABLES_CMD_PATCH.addOpByName = (name) =>
{
    new ModalDialog({
        "prompt": true,
        "title": "Add Op",
        "text": "Enter full op name",
        "promptValue": name,
        "promptOk": (opname) =>
        {
            gui.serverOps.loadOpDependencies(opname, function ()
            {
                gui.patchView.addOp(opname, { "onOpAdd": (op) =>
                {
                    op.setUiAttrib({
                        "translate": {
                            "x": gui.patchView.patchRenderer.viewBox.mousePatchX,
                            "y": gui.patchView.patchRenderer.viewBox.mousePatchY },
                    });
                    if (op)
                    {
                        gui.patchView.focusOp(op.id);
                    }
                } });
            });
        }
    });
};



CABLES_CMD_PATCH.reloadOp = function (x, y)
{
    const ops = gui.patchView.getSelectedOps();
    if (!ops.length) return;

    const op = gui.patchView.getSelectedOps()[0];

    gui.serverOps.execute(op.opId, () =>
    {
        CABLES.UI.notify("reloaded op " + op.objName);
    });
};


CABLES_CMD_PATCH.addOp = function (x, y)
{
    gui.opSelect().show({ "x": 0, "y": 0 });
};

CABLES_CMD_PATCH.patchWebsite = function ()
{
    window.open(CABLES.platform.getCablesUrl() + "/p/" + gui.project().shortId || gui.project()._id);
};

CABLES_CMD_PATCH.renameVariable = function (oldname)
{
    new ModalDialog({
        "prompt": true,
        "title": "Rename Variable",
        "text": "Enter a new name for the variable " + oldname,
        "promptValue": oldname,
        "promptOk": (newname) =>
        {
            gui.corePatch().emitEvent("variableRename", oldname, newname);
            gui.corePatch().deleteVar(oldname);
        } });
};

CABLES_CMD_PATCH.createVariable = function (op)
{
    new ModalDialog({
        "prompt": true,
        "title": "New Variable",
        "text": "Enter a name for the new variable",
        "promptValue": "",
        "promptOk": (str) =>
        {
            if (op)
            {
                op.setTitle(str);
                op.varName.set(str);
                gui.opParams.show(op);
            }
        } });
};

CABLES_CMD_PATCH.createVarNumber = function (next)
{
    new ModalDialog({
        "prompt": true,
        "title": "New Variable",
        "text": "Enter a name for the new variable",
        "promptValue": "myNewVar",
        "promptOk": (str) =>
        {
            const opSetter = gui.patchView.addOp(CABLES.UI.DEFAULTOPNAMES.VarSetNumber);
            const opGetter = gui.patchView.addOp(CABLES.UI.DEFAULTOPNAMES.VarGetNumber);

            opSetter.varName.set(str);
            opGetter.varName.set(str);
        } });
};

CABLES_CMD_PATCH.analyze = function (force)
{
    new AnalyzePatchTab();
};

CABLES_CMD_PATCH._createVariable = function (name, p, p2, value, next)
{
    const getsetOp = CABLES.UI.getVarGetterOpNameByType(p.type, p);

    let portName = getsetOp.portName;
    let portNameOut = getsetOp.portNameOut;
    let opSetterName = getsetOp.setter;
    let opGetterName = getsetOp.getter;

    gui.patchView.addOp(opSetterName, { "onOpAdd": (opSetter) =>
    {
        gui.patchView.addOp(opGetterName, { "onOpAdd": (opGetter) =>
        {
            opSetter.uiAttr({ "subPatch": gui.patchView.getCurrentSubPatch() });
            opGetter.uiAttr({ "subPatch": gui.patchView.getCurrentSubPatch() });

            if (p.type != CABLES.OP_PORT_TYPE_FUNCTION)
                opSetter.getPortByName(portName).set(value);

            if (p.direction == CABLES.PORT_DIR_IN)
            {
                p.op.patch.link(opGetter, portName, p.op, p.name);
                if (p2) p2.op.patch.link(opSetter, portNameOut, p2.op, p2.name);
            }
            else
            {
                p.op.patch.link(opSetter, portName, p.op, p.name);
                if (p2) p2.op.patch.link(opGetter, portNameOut, p2.op, p2.name);
            }

            opSetter.varName.set(name);
            opGetter.varName.set(name);

            gui.patchView.setCurrentSubPatch(gui.patchView.getCurrentSubPatch());

            if (next)next(opSetter, opGetter);

            gui.closeModal();
        } });
    } });
};

CABLES_CMD_PATCH.replaceLinkTriggerReceiveExist = function ()
{
    const link = CABLES.UI.OPSELECT.linkNewLink;
    const p = link.portIn;
    const portOut = link.portOut;
    CABLES.UI.OPSELECT.linkNewLink = null;

    gui.opSelect().close();
    gui.closeModal();
    const getsetOp = CABLES.UI.getVarGetterOpNameByType(p.type, p);

    gui.patchView.addOp(
        getsetOp.getter,
        { "onOpAdd": (opGetter) =>
        {
            link.remove();
            p.removeLinks();
            p.op.patch.link(opGetter, getsetOp.portNameOut, p.op, p.name);

            opGetter.uiAttr({
                "subPatch": gui.patchView.getCurrentSubPatch(),
                "translate": {
                    "x": p.op.uiAttribs.translate.x + 20,
                    "y": p.op.uiAttribs.translate.y - 40
                } });
        } });
};

CABLES_CMD_PATCH.createTriggerSendReceiveExist = function ()
{
    const type = CABLES.UI.OPSELECT.linkNewOpToPort.type;
    const p = CABLES.UI.OPSELECT.linkNewOpToPort;

    gui.opSelect().close();
    gui.closeModal();
    const getsetOp = CABLES.UI.getVarGetterOpNameByType(type, p);
    CABLES.UI.OPSELECT.linkNewOpToPort = null;

    let getset = getsetOp.setter;
    if (p.direction == CABLES.PORT_DIR_IN)getset = getsetOp.getter;

    gui.patchView.addOp(
        getset,
        { "onOpAdd": (op) =>
        {
            let off = -40;

            if (p.direction == CABLES.PORT_DIR_IN)
            {
                p.op.patch.link(op, getsetOp.portNameOut, p.op, p.name);
            }
            else
            {
                p.op.patch.link(op, getsetOp.portName, p.op, p.name);
                off *= -1;
            }

            op.uiAttr({
                "subPatch": gui.patchView.getCurrentSubPatch(),
                "translate": {
                    "x": p.op.uiAttribs.translate.x + 20,
                    "y": p.op.uiAttribs.translate.y + off
                } });
        } });
};

CABLES_CMD_PATCH.replaceLinkVariableExist = function ()
{
    const link = CABLES.UI.OPSELECT.linkNewLink;
    const p = link.portIn;
    const portOut = link.portOut;
    CABLES.UI.OPSELECT.linkNewLink = null;

    gui.opSelect().close();
    gui.closeModal();
    const getsetOp = CABLES.UI.getVarGetterOpNameByType(p.type, p);

    gui.patchView.addOp(
        getsetOp.getter,
        { "onOpAdd": (opGetter) =>
        {
            link.remove();
            p.removeLinks();

            p.op.patch.link(opGetter, getsetOp.portName, p.op, p.name);

            opGetter.uiAttr({
                "subPatch": gui.patchView.getCurrentSubPatch(),
                "translate": {
                    "x": p.op.uiAttribs.translate.x + 20,
                    "y": p.op.uiAttribs.translate.y - 40
                } });
        } });
};


CABLES_CMD_PATCH.addLinkReroute = function ()
{
    const link = CABLES.UI.OPSELECT.linkNewLink;
    const p = link.portIn;
    const portOut = link.portOut;
    CABLES.UI.OPSELECT.linkNewLink = null;

    gui.opSelect().close();
    gui.closeModal();
    const getsetOp = defaultOps.getRerouteOp(p.type);

    gui.patchView.addOp(
        getsetOp,
        { "onOpAdd": (opGetter) =>
        {
            const glPatch = gui.patchView.patchRenderer;
            let x = glPatch._lastMouseX;
            let y = glPatch._lastMouseY;

            opGetter.uiAttr({
                "subPatch": gui.patchView.getCurrentSubPatch(),
            });

            setTimeout(
                () =>
                {
                    x = glPatch.snap.snapX(x);
                    y = glPatch.snap.snapY(y);

                    gui.patchView.insertOpInLink(link, opGetter, x, y);
                }, 100);
        } });
};


CABLES_CMD_PATCH.createLinkVariableExist = function (createTrigger = false)
{
    gui.opSelect().close();
    const type = CABLES.UI.OPSELECT.linkNewOpToPort.type;
    const p = CABLES.UI.OPSELECT.linkNewOpToPort;

    gui.closeModal();
    const getsetOp = CABLES.UI.getVarGetterOpNameByType(type, p);
    CABLES.UI.OPSELECT.linkNewOpToPort = null;

    let opFunction = getsetOp.getter;
    let newOpX = p.op.uiAttribs.translate.x + 20;
    let newOpY = p.op.uiAttribs.translate.y - 40;
    if (p.direction === CABLES.PORT_DIR_OUT)
    {
        if (createTrigger)
        {
            opFunction = getsetOp.setTrigger;
        }
        else
        {
            opFunction = getsetOp.setter;
        }
        newOpY = p.op.uiAttribs.translate.y + 40;
    }

    gui.patchView.addOp(
        opFunction,
        { "onOpAdd": (opGetter) =>
        {
            p.removeLinks();
            p.op.patch.link(opGetter, getsetOp.portName, p.op, p.name);

            opGetter.uiAttr({
                "subPatch": gui.patchView.getCurrentSubPatch(),
                "translate": {
                    "x": newOpX,
                    "y": newOpY
                } });
        } });
};

CABLES_CMD_PATCH.replaceLinkVariable = function ()
{
    gui.opSelect().close();
    new ModalDialog({
        "prompt": true,
        "title": "New Variable",
        "text": "Enter a name for the new variable",
        "promptValue": "",
        "promptOk": (str) =>
        {
            const link = CABLES.UI.OPSELECT.linkNewLink;

            const p1 = link.portIn;
            const p2 = link.portOut;
            CABLES.UI.OPSELECT.linkNewLink = null;

            if (p1.direction == CABLES.PORT_DIR_IN)p1.removeLinks();
            else p2.removeLinks();

            link.remove();

            CABLES_CMD_PATCH._createVariable(str, p2, p1, p2.get(), (setter, getter) =>
            {
                getter.uiAttr({ "translate": {
                    "x": p1.op.uiAttribs.translate.x,
                    "y": p1.op.uiAttribs.translate.y - 40
                } });

                setter.uiAttr({ "translate": {
                    "x": p2.op.uiAttribs.translate.x,
                    "y": p2.op.uiAttribs.translate.y + 40
                } });
            });
        } });
};

CABLES_CMD_PATCH.createTriggerSendReceive = () =>
{
    gui.opSelect().close();
    const link = CABLES.UI.OPSELECT.linkNewLink;

    new ModalDialog({
        "prompt": true,
        "title": "New Trigger Send",
        "text": "Enter a name for a new wireless trigger",
        "promptValue": link.portOut.name,
        "promptOk": (str) =>
        {
            const p1 = link.portIn;
            const p2 = link.portOut;
            CABLES.UI.OPSELECT.linkNewLink = null;

            if (p1.direction == CABLES.PORT_DIR_IN)p1.removeLinks();
            else p2.removeLinks();

            link.remove();

            CABLES_CMD_PATCH._createVariable(str, p2, p1, p2.get(), (setter, getter) =>
            {
                getter.uiAttr({
                    "subPatch": gui.patchView.getCurrentSubPatch(),
                    "translate": {
                        "x": p1.op.uiAttribs.translate.x,
                        "y": p1.op.uiAttribs.translate.y - 40
                    } });

                setter.uiAttr({
                    "subPatch": gui.patchView.getCurrentSubPatch(),
                    "translate": {
                        "x": p2.op.uiAttribs.translate.x,
                        "y": p2.op.uiAttribs.translate.y + 40
                    } });
            });
        } });
};

CABLES_CMD_PATCH.createAutoVariable = function ()
{
    gui.opSelect().close();
    const p = CABLES.UI.OPSELECT.linkNewOpToPort;

    new ModalDialog({
        "prompt": true,
        "title": "New Variable",
        "text": "Enter a name for the new variable",
        "promptValue": p.name,
        "promptOk": (str) =>
        {
            CABLES_CMD_PATCH._createVariable(str, p, null, p.get(), (setter, getter) =>
            {
                if (!setter.uiAttribs.translate)
                    setter.uiAttr({
                        "subPatch": gui.patchView.getCurrentSubPatch(),
                        "translate": {
                            "x": p.op.uiAttribs.translate.x,
                            "y": p.op.uiAttribs.translate.y + 40
                        } });

                getter.uiAttr({
                    "subPatch": gui.patchView.getCurrentSubPatch(),
                    "translate": {
                        "x": setter.uiAttribs.translate.x,
                        "y": setter.uiAttribs.translate.y + 40
                    } });
            });
        } });
};

CABLES_CMD_PATCH.addSpaceX = () =>
{
    gui.patchView.addSpaceBetweenOpsX();
};

CABLES_CMD_PATCH.addSpaceY = () =>
{
    gui.patchView.addSpaceBetweenOpsY();
};

CABLES_CMD_PATCH.linkTwoSelectedOps = () =>
{
    if (gui.patchView.getSelectedOps().length != 2)
    {
        console.log("needs 2 selected ops");
        return;
    }

    let a = gui.patchView.getSelectedOps()[0];
    let b = gui.patchView.getSelectedOps()[1];

    if (a.uiAttribs.translate.y > b.uiAttribs.translate.y) gui.patchView.suggestionBetweenTwoOps(b, a);
    else gui.patchView.suggestionBetweenTwoOps(a, b);
};

CABLES_CMD_PATCH.compressOps = () =>
{
    gui.patchView.compressSelectedOps(gui.patchView.getSelectedOps());
};

CABLES_CMD_PATCH.alignOpsLeft = () =>
{
    gui.patchView.alignSelectedOpsVert(gui.patchView.getSelectedOps());
};



CABLES_CMD_PATCH.upGradeOps = function ()
{
    const selops = gui.patchView.getSelectedOps();
    for (let i = 0; i < selops.length; i++)
    {
        const opdoc = gui.opDocs.getOpDocById(selops[i].opId);
        if (opdoc && opdoc.oldVersion && opdoc.newestVersion && opdoc.newestVersion.name)
            gui.patchView.replaceOp(selops[i].id, opdoc.newestVersion.name);
    }
};

CABLES_CMD_PATCH.downGradeOp = function ()
{
    const selops = gui.patchView.getSelectedOps();
    for (let i = 0; i < selops.length; i++)
    {
        gui.patchView.downGradeOp(selops[i].id, selops[i].objName);
    }
};

CABLES_CMD_PATCH.watchGlOp = function ()
{
    new GlOpWatcher(gui.mainTabs);
    gui.maintabPanel.show(true);
};


CABLES_CMD_PATCH.savePatchScreenshot = function ()
{
    gui.patchView.patchRenderer._cgl.saveScreenshot("patchfield_" + performance.now(), () =>
    {
        gui.patchView.patchRenderer._cgl.patch.resume();
    });
};

CABLES_CMD_PATCH.toggleResizable = function ()
{
    const ops = gui.patchView.getSelectedOps();

    for (let i = 0; i < ops.length; i++)
    {
        const op = ops[i];

        op.setUiAttribs({
            "stretchPorts": !op.uiAttribs.stretchPorts,
            "resizableY": false,
            "resizable": !op.uiAttribs.stretchPorts
        });
    }
};


CABLES_CMD_PATCH.setOpTitle = function ()
{
    const ops = gui.patchView.getSelectedOps();

    if (ops.length != 1)
    {
        console.warn("rename canceled - select one op!");
        return;
    }

    new ModalDialog({
        "prompt": true,
        "title": "Set Title",
        "text": "Enter a title for this op",
        "promptValue": ops[0].name,
        "promptOk": (name) =>
        {
            gui.opParams.setCurrentOpTitle(name);
        } });
};

CABLES_CMD_PATCH.resume = function ()
{
    gui.corePatch().resume();
};

CABLES_CMD_PATCH.pause = function ()
{
    gui.corePatch().pause();
};

CABLES_CMD_PATCH.replaceFilePath = function ()
{
    new ModalDialog({
        "prompt": true,
        "title": "Replace String Values",
        "text": "Search for...",
        "promptValue": "/assets/",
        "promptOk": (srch) =>
        {
            new ModalDialog({
                "prompt": true,
                "title": "Replace String Values",
                "text": "...replace with",
                "promptValue": "/assets/" + gui.project()._id,
                "promptOk": (rplc) =>
                {
                    const ops = gui.corePatch().ops;
                    for (let i = 0; i < ops.length; i++)
                    {
                        for (let j = 0; j < ops[i].portsIn.length; j++)
                        {
                            if (ops[i].portsIn[j].uiAttribs && ops[i].portsIn[j].uiAttribs.display && ops[i].portsIn[j].uiAttribs.display == "file")
                            {
                                console.log("filename:", ops[i].portsIn[j].get());
                                let v = ops[i].portsIn[j].get();

                                if (v) console.log("srch index", v.indexOf(srch));
                                if (v && v.indexOf(srch) == 0)
                                {
                                    console.log("found str!");
                                    v = rplc + v.substring(srch.length);
                                    ops[i].portsIn[j].set(v);
                                    console.log("result filename:", v);
                                }
                            }
                        }
                    }
                } });
        } });
};

CABLES_CMD_PATCH.convertBlueprintToSubpatch = function (blueprint, skipSelection = false)
{
    const patch = gui.corePatch();
    const ops = patch.ops;
    const relevantOps = [];
    for (let i = 0; i < ops.length; i++)
    {
        const op = ops[i];
        if (op.uiAttribs && op.uiAttribs.blueprintOpId === blueprint.id) relevantOps.push(op);
    }

    let hiddenSubPatchOp = null;
    relevantOps.forEach((op) =>
    {
        if (op.objName && op.isSubPatchOp())
        {
            op.uiAttribs.translate = {
                "x": blueprint.uiAttribs.translate.x,
                "y": blueprint.uiAttribs.translate.y
            };
            op.portsIn.forEach(
                (portIn) =>
                {
                    const bpPort = blueprint.getPortByName(portIn.name);
                    if (!bpPort) return;
                    if (bpPort.isLinked())
                    {
                        bpPort.links.forEach((bpLink) =>
                        {
                            const parent = bpLink.portOut.op;
                            patch.link(parent, bpLink.portOut.name, op, portIn.name);
                        });
                    }
                    else
                    {
                        portIn.set(bpPort.get());
                    }
                });
            op.portsOut.forEach(
                (portOut) =>
                {
                    const bpPort = blueprint.getPortByName(portOut.name);
                    if (!bpPort) return;
                    if (bpPort.isLinked())
                    {
                        bpPort.links.forEach((bpLink) =>
                        {
                            const parent = bpLink.portIn.op;
                            const link = patch.link(op, portOut.name, parent, bpLink.portIn.name);
                        });
                    }
                    else
                    {
                        portOut.set(bpPort.get());
                    }
                });
        }


        if (op.storage) delete op.storage.blueprint;
        delete op.uiAttribs.blueprintOpId;

        if (op.uiAttribs && op.uiAttribs.hidden)
        {
            if (op.objName && op.isSubPatchOp())
            {
                hiddenSubPatchOp = op;
                op.rebuildListeners();
            }

            op.setUiAttrib({ "hidden": false });
        }
    });
    patch.deleteOp(blueprint.id, false, false);
    if (!skipSelection && hiddenSubPatchOp)
    {
        gui.patchView.unselectAllOps();
        gui.patchView.selectOpId(hiddenSubPatchOp.id);
    }
};

CABLES_CMD_PATCH.replaceOp = function ()
{
    new ModalDialog({
        "prompt": true,
        "title": "Replace Ops",
        "text": "Replace selected ops with: Enter full op name",
        "promptOk": (opname) =>
        {
            const ops = gui.patchView.getSelectedOps();
            for (let i = 0; i < ops.length; i++)
            {
                gui.patchView.replaceOp(ops[i].id, opname);
            }
        }
    });
};

CABLES_CMD_PATCH.editOpSummary = function (opId, opName, oldSummary = "")
{
    if (!CABLES.platform.frontendOptions.editOpSummary) return;

    new ModalDialog({
        "prompt": true,
        "title": opName,
        "text": "New summary:",
        "promptValue": oldSummary,
        "promptOk": (summary) =>
        {
            gui.savingTitleAnimStart("Updating Op...");
            CABLESUILOADER.talkerAPI.send("opSetSummary", { "id": opId, "name": opName, "summary": summary }, (err, res) =>
            {
                if (!err)
                {
                    gui.serverOps.loadOpDependencies(opName, () =>
                    {
                        gui.savingTitleAnimEnd();
                        gui.emitEvent("refreshManageOp", opName);
                    }, true);
                }
            });
        }
    });
};

CABLES_CMD_PATCH.uncollideOps = function (ops)
{
    let found = true;
    // while (found)

    for (let i = 0; i < gui.corePatch().ops.length; i++)
    {
        const op = gui.corePatch().ops[i];

        if (!op.uiAttribs.translate)
            op.uiAttribs.translate = { "x": 0, "y": 0 };

        for (let j = 0; j < gui.corePatch().ops.length; j++)
        {
            const b = gui.corePatch().ops[j];
            if (b.deleted || b == op) continue;

            while (b.uiAttribs.translate &&
                op.uiAttribs.translate &&
                (op.uiAttribs.translate.x <= b.uiAttribs.translate.x + 50 && op.uiAttribs.translate.x >= b.uiAttribs.translate.x) &&
                op.uiAttribs.translate.y == b.uiAttribs.translate.y)
            {
                op.setUiAttrib({ "translate": { "x": b.uiAttribs.translate.x, "y": b.uiAttribs.translate.y + CABLES.GLUI.glUiConfig.newOpDistanceY } });
                found = true;
            }
        }
    }
};



CABLES_CMD_PATCH.togglePatchLike = (targetElement = null) =>
{
    CABLESUILOADER.talkerAPI.send("toggleFav", {}, (err, res) =>
    {
        if (!err && res.success && targetElement)
        {
            const icon = targetElement.querySelector(".icon");
            if (icon)
            {
                if (res.favstate)
                {
                    icon.classList.remove("icon-heart");
                    icon.classList.add("icon-heart-fill");
                }
                else
                {
                    icon.classList.remove("icon-heart-fill");
                    icon.classList.add("icon-heart");
                }
            }
        }
    });
};

CABLES_CMD_PATCH.cloneSelectedOps = (ops) =>
{
    if (!ops)
    {
        ops = gui.patchView.getSelectedOps();

        for (let i = 0; i < ops.length; i++)
        {
            const op = ops[i];
            const opname = op.objName;
            let sanitizedOpName = opname.replaceAll(".", "_");


            let newOpname = "Ops.Patch.P" + gui.patchId + "." + sanitizedOpName;
            newOpname = newOpname.replaceAll(".Ops_", ".");

            const newOpnameNoVer = newOpname.replaceAll("_v", "V");

            let count = 0;
            newOpname = newOpnameNoVer;
            while (gui.opDocs.getOpDocByName(newOpname))
            {
                newOpname = newOpnameNoVer + count;
                count++;
            }
            op.renameopto = newOpname;

            console.log("new renameto name:", newOpname);
        }

        if (ops.length == 0) return;
    }

    // loadingModal = loadingModal || gui.startModalLoading("Cloning ops...");


    if (ops.length == 0)
    {
        gui.endModalLoading();
        return;
    }
    const op = ops.pop();
    const opname = op.objName;
    const newOpname = op.renameopto;

    if (gui.opDocs.getOpDocByName(newOpname))
    {
        // that opname was already renamed in list
        gui.patchView.replaceOp(op.id, newOpname);
        CABLES_CMD_PATCH.cloneSelectedOps(ops);
    }
    else
    {
        gui.serverOps.clone(op.opId, newOpname, () =>
        {
            gui.serverOps.loadOpDependencies(opname, function ()
            {
                gui.patchView.replaceOp(op.id, newOpname);

                CABLES.UI.notify("created op " + newOpname, null, { "force": true });

                CABLES_CMD_PATCH.cloneSelectedOps(ops);
            });
        }, { "openEditor": false });
    }
};

CABLES_CMD_PATCH.renameOp = (opName = null) =>
{
    if (!opName)
    {
        const ops = gui.patchView.getSelectedOps();
        if (!ops.length) return;
        const op = gui.patchView.getSelectedOps()[0];
        opName = op.objName;
    }

    if (CABLES.platform.frontendOptions.opRenameInEditor)
    {
        gui.serverOps.renameDialog(opName);
    }
    else
    {
        gui.serverOps.renameDialogIframe(opName);
    }
};

CABLES_CMD_PATCH.deleteOp = (opName = null) =>
{
    if (!opName)
    {
        const ops = gui.patchView.getSelectedOps();
        if (!ops.length) return;
        const op = gui.patchView.getSelectedOps()[0];
        opName = op.objName;
    }

    if (CABLES.platform.frontendOptions.opDeleteInEditor)
    {
        gui.serverOps.deleteDialog(opName);
    }
};

CMD_PATCH_COMMANDS.push(
    {
        "cmd": "Select all ops",
        "category": "patch",
        "func": CABLES_CMD_PATCH.selectAllOps,
        "hotkey": "CMD + a"
    },
    {
        "cmd": "Delete selected ops",
        "category": "patch",
        "func": CABLES_CMD_PATCH.deleteSelectedOps,
        "icon": "trash",
        "hotkey": "DEL"
    },
    {
        "cmd": "Save patch",
        "category": "patch",
        "func": CABLES_CMD_PATCH.save,
        "icon": "save",
        "hotkey": "[cmd_ctrl]`S`",
        "infotext": "cmd_savepatch"

    },
    {
        "cmd": "Save patch as...",
        "category": "patch",
        "func": CABLES_CMD_PATCH.saveAs,
        "icon": "save",
        "hotkey": "[cmd_ctrl][shift]`s`",
    },
    {
        "cmd": "Upload file dialog",
        "category": "patch",
        "func": CABLES_CMD_PATCH.uploadFileDialog,
        "icon": "file",
        "frontendOption": "uploadFiles"
    },
    {
        "cmd": "Upload file",
        "category": "patch",
        "func": CABLES_CMD_PATCH.uploadFile,
        "icon": "file",
        "frontendOption": "uploadFiles"
    },
    {
        "cmd": "Create new file",
        "category": "patch",
        "func": CABLES_CMD_PATCH.createFile,
        "icon": "file",
        "frontendOption": "uploadFiles"
    },
    {
        "cmd": "Select child ops",
        "category": "op",
        "func": CABLES_CMD_PATCH.selectChilds
    },
    {
        "cmd": "Clear op titles",
        "category": "op",
        "func": CABLES_CMD_PATCH.clearOpTitles
    },
    // {
    //     "cmd": "Create subpatch",
    //     "category": "patch",
    //     "func": CABLES_CMD_PATCH.createSubPatchFromSelection,
    //     "icon": "subpatch"
    // },
    {
        "cmd": "Export static html",
        "category": "patch",
        "func": CABLES_CMD_PATCH.export,
        "icon": "download",
        "frontendOption": "showExport"
    },
    {
        "cmd": "Show backups",
        "category": "patch",
        "func": CABLES_CMD_PATCH.showBackups,
        "icon": "file",
        "frontendOption": "showPatchBackups"
    },
    {
        "cmd": "Create new patch",
        "category": "patch",
        "func": CABLES_CMD_PATCH.newPatch,
        "icon": "file"
    },
    {
        "cmd": "Reload op",
        "category": "patch",
        "func": CABLES_CMD_PATCH.reloadOp,
        "icon": "op",
        "infotext": "cmd_reloadop"
    },
    {
        "cmd": "Add op",
        "category": "patch",
        "func": CABLES_CMD_PATCH.addOp,
        "icon": "op",
        "infotext": "cmd_addop"
    },
    {
        "cmd": "Add op by name",
        "category": "patch",
        "func": CABLES_CMD_PATCH.addOpByName,
        "icon": "op"
    },
    {
        "cmd": "Edit op",
        "category": "op",
        "func": CABLES_CMD_PATCH.editOp,
        "icon": "edit"
    },
    {
        "cmd": "Set title",
        "category": "op",
        "func": CABLES_CMD_PATCH.setOpTitle,
        "icon": "edit"
    },
    {
        "cmd": "Toggle op resizable",
        "category": "op",
        "func": CABLES_CMD_PATCH.toggleResizable,
        "icon": "op"
    },
    {
        "cmd": "Clear patch",
        "category": "patch",
        "func": CABLES_CMD_PATCH.clear
    },
    {
        "cmd": "Open patch website",
        "category": "patch",
        "func": CABLES_CMD_PATCH.patchWebsite,
        "icon": "link",
        "frontendOption": "hasCommunity"
    },
    {
        "cmd": "Pause patch execution",
        "category": "patch",
        "func": CABLES_CMD_PATCH.pause
    },
    {
        "cmd": "Resume patch execution",
        "category": "patch",
        "func": CABLES_CMD_PATCH.resume
    },
    {
        "cmd": "Replace file path",
        "category": "patch",
        "func": CABLES_CMD_PATCH.replaceFilePath
    },
    {
        "cmd": "Find unconnected ops",
        "category": "patch",
        "func": CABLES_CMD_PATCH.findUnconnectedOps
    },
    {
        "cmd": "Find user ops",
        "category": "patch",
        "func": CABLES_CMD_PATCH.findUserOps
    },
    {
        "cmd": "Find commented ops",
        "category": "patch",
        "func": CABLES_CMD_PATCH.findCommentedOps
    },
    {
        "cmd": "Find external assets",
        "category": "patch",
        "func": CABLES_CMD_PATCH.findOpsUsingExternalAssets
    },
    {
        "cmd": "Analyze patch",
        "category": "patch",
        "func": CABLES_CMD_PATCH.analyze
    },
    {
        "cmd": "Create number variable",
        "category": "patch",
        "func": CABLES_CMD_PATCH.createVarNumber
    },
    {
        "cmd": "Create backup",
        "category": "patch",
        "func": CABLES_CMD_PATCH.createBackup
    },
    {
        "cmd": "Align ops left",
        "func": CABLES_CMD_PATCH.alignOpsLeft,
        "icon": "align-left"
    },
    {
        "cmd": "Compress ops vertically",
        "func": CABLES_CMD_PATCH.compressOps,
        "icon": "list"
    },
    {
        "cmd": "Add space x",
        "func": CABLES_CMD_PATCH.addSpaceX,
        "icon": "list"
    },
    {
        "cmd": "Add space y",
        "func": CABLES_CMD_PATCH.addSpaceY,
        "icon": "list"
    },
    {
        "cmd": "Save patchfield screenshot",
        "func": CABLES_CMD_PATCH.savePatchScreenshot,
        "icon": "image"
    },
    {
        "cmd": "Replace ops",
        "func": CABLES_CMD_PATCH.replaceOp,
        "icon": "op"
    },
    {
        "cmd": "Link two selected ops",
        "func": CABLES_CMD_PATCH.linkTwoSelectedOps,
        "icon": "op"
    },
    {
        "cmd": "Downgrade selected op",
        "func": CABLES_CMD_PATCH.downGradeOp,
        "icon": "op"
    },
    {
        "cmd": "Upgrade selected ops",
        "func": CABLES_CMD_PATCH.upGradeOps,
        "icon": "op"
    },
    {
        "cmd": "Clone selected op",
        "func": CABLES_CMD_PATCH.cloneSelectedOp,
        "category": "patch",
        "icon": "op"
    },
    {
        "cmd": "Clone selected ops to patch ops",
        "func": CABLES_CMD_PATCH.cloneSelectedOps,
        "category": "patch",
        "icon": "op"
    },
    {
        "cmd": "Create new version of op",
        "func": CABLES_CMD_PATCH.createVersionSelectedOp,
        "icon": "op"
    },
    {
        "cmd": "Manage selected op",
        "func": CABLES_CMD_PATCH.manageSelectedOp,
        "icon": "op"
    },
    {
        "cmd": "Go to parent subpatch",
        "func": CABLES_CMD_PATCH.gotoParentSubpatch,
    },
    {
        "cmd": "Open params in tab",
        "func": CABLES_CMD_PATCH.openParamsTab,
        "category": "patch",
        "icon": "op"
    },
    {
        "cmd": "Point blueprints to local patch",
        "func": CABLES_CMD_PATCH.localizeBlueprints,
        "category": "patch",
        "icon": "op"
    },
    {
        "cmd": "Show glop information",
        "func": CABLES_CMD_PATCH.watchGlOp,
        "category": "patch",
        "icon": "op"
    },
    {
        "cmd": "Uncollide ops",
        "func": CABLES_CMD_PATCH.uncollideOps,
        "category": "patch",
        "icon": "op"
    },
    {
        "cmd": "Toggle patch like",
        "func": CABLES_CMD_PATCH.togglePatchLike,
        "category": "patch"
    },
    {
        "cmd": "Create subpatch op",
        "func": CABLES_CMD_PATCH.createSubPatchOp,
        "category": "patch",
        "icon": "op"
    },
    {
        "cmd": "Delete unused patch ops",
        "func": CABLES_CMD_PATCH.deleteUnusedPatchOps,
        "category": "patch",
        "icon": "op"
    },
    {
        "cmd": "Center ops in subpatch",
        "func": CABLES_CMD_PATCH.centerOpsInSubpatch,
        "category": "patch",
        "icon": "op"
    },
    {
        "cmd": "Set patch title",
        "func": CABLES_CMD_PATCH.setPatchTitle,
        "category": "patch",
        "icon": "edit"
    },
    {
        "cmd": "Rename op",
        "func": CABLES_CMD_PATCH.renameOp,
        "category": "op",
        "icon": "op"
    },
    {
        "cmd": "Auto position subpatch input output ops",
        "func": CABLES_CMD_PATCH.autoPosSubpatchInputOutputOps,
        "category": "op",
        "icon": "op"
    },
    {
        "cmd": "Reload patch",
        "category": "patch",
        "func": CABLES_CMD_PATCH.reload
    },

);