Signed-off-by: falkTX <falktx@falktx.com>tags/v2.2.0-RC1
@@ -1141,7 +1141,17 @@ typedef enum { | |||
* @a value1 Parameter index | |||
* @a valueStr New mapped range as "%f:%f" syntax | |||
*/ | |||
ENGINE_CALLBACK_PARAMETER_MAPPED_RANGE_CHANGED = 46 | |||
ENGINE_CALLBACK_PARAMETER_MAPPED_RANGE_CHANGED = 46, | |||
/*! | |||
* A patchbay client position has changed. | |||
* @a pluginId Client Id | |||
* @a value1 X position 1 | |||
* @a value2 Y position 1 | |||
* @a value3 X position 2 | |||
* @a valuef Y position 2 | |||
*/ | |||
ENGINE_CALLBACK_PATCHBAY_CLIENT_POSITION_CHANGED = 47, | |||
} EngineCallbackOpcode; | |||
@@ -1168,6 +1168,12 @@ public: | |||
*/ | |||
virtual bool patchbayDisconnect(bool external, uint connectionId); | |||
/*! | |||
* Set the position of a group. | |||
*/ | |||
virtual bool patchbaySetGroupPos(bool sendHost, bool sendOSC, bool external, | |||
uint groupId, int x1, int y1, int x2, int y2); | |||
/*! | |||
* Force the engine to resend all patchbay clients, ports and connections again. | |||
*/ | |||
@@ -1348,10 +1354,11 @@ protected: | |||
* Virtual functions for handling patchbay state. | |||
* Do not free returned data. | |||
*/ | |||
struct PatchbayPosition { const char* name; int x1, y1, x2, y2; bool dealloc; }; | |||
virtual const char* const* getPatchbayConnections(bool external) const; | |||
virtual void restorePatchbayConnection(bool external, | |||
const char* sourcePort, | |||
const char* targetPort); | |||
virtual const PatchbayPosition* getPatchbayPositions(bool external, uint& count) const; | |||
virtual void restorePatchbayConnection(bool external, const char* sourcePort, const char* targetPort); | |||
virtual void restorePatchbayGroupPosition(bool external, const PatchbayPosition& ppos); | |||
/*! | |||
* Virtual functions for handling external graph ports. | |||
@@ -550,6 +550,15 @@ CARLA_EXPORT bool carla_patchbay_connect(CarlaHostHandle handle, bool external, | |||
*/ | |||
CARLA_EXPORT bool carla_patchbay_disconnect(CarlaHostHandle handle, bool external, uint connectionId); | |||
/*! | |||
* Set the position of a group. | |||
* This is purely cached and saved in the project file, Carla backend does nothing with the value. | |||
* When loading a project, callbacks are used to inform of the previously saved positions. | |||
* @see ENGINE_CALLBACK_PATCHBAY_CLIENT_POSITION_CHANGED | |||
*/ | |||
CARLA_EXPORT bool carla_patchbay_set_group_pos(CarlaHostHandle handle, bool external, | |||
uint groupId, int x1, int y1, int x2, int y2); | |||
/*! | |||
* Force the engine to resend all patchbay clients, ports and connections again. | |||
* @param external Wherever to show external/hardware ports instead of internal ones. | |||
@@ -970,6 +970,16 @@ bool carla_patchbay_disconnect(CarlaHostHandle handle, bool external, uint conne | |||
return handle->engine->patchbayDisconnect(external, connectionId); | |||
} | |||
bool carla_patchbay_set_group_pos(CarlaHostHandle handle, bool external, uint groupId, int x1, int y1, int x2, int y2) | |||
{ | |||
CARLA_SAFE_ASSERT_WITH_LAST_ERROR_RETURN(handle->engine != nullptr, "Engine is not initialized", false); | |||
carla_debug("carla_patchbay_set_group_pos(%p, %s, %u, %i, %i, %i, %i)", | |||
handle, bool2str(external), groupId, x1, y1, x2, y2); | |||
return handle->engine->patchbaySetGroupPos(true, false, external, groupId, x1, y1, x2, y2); | |||
} | |||
bool carla_patchbay_refresh(CarlaHostHandle handle, bool external) | |||
{ | |||
CARLA_SAFE_ASSERT_WITH_LAST_ERROR_RETURN(handle->engine != nullptr, "Engine is not initialized", false); | |||
@@ -2198,67 +2198,149 @@ void CarlaEngine::saveProjectInternal(water::MemoryOutputStream& outStream) cons | |||
// save internal connections | |||
if (pData->options.processMode == ENGINE_PROCESS_MODE_PATCHBAY) | |||
{ | |||
if (const char* const* const patchbayConns = getPatchbayConnections(false)) | |||
uint posCount = 0; | |||
const char* const* const patchbayConns = getPatchbayConnections(false); | |||
const PatchbayPosition* const patchbayPos = getPatchbayPositions(false, posCount); | |||
if (patchbayConns != nullptr || patchbayPos != nullptr) | |||
{ | |||
MemoryOutputStream outPatchbay(2048); | |||
outPatchbay << "\n <Patchbay>\n"; | |||
for (int i=0; patchbayConns[i] != nullptr && patchbayConns[i+1] != nullptr; ++i, ++i ) | |||
if (patchbayConns != nullptr) | |||
{ | |||
const char* const connSource(patchbayConns[i]); | |||
const char* const connTarget(patchbayConns[i+1]); | |||
for (int i=0; patchbayConns[i] != nullptr && patchbayConns[i+1] != nullptr; ++i, ++i) | |||
{ | |||
const char* const connSource(patchbayConns[i]); | |||
const char* const connTarget(patchbayConns[i+1]); | |||
CARLA_SAFE_ASSERT_CONTINUE(connSource != nullptr && connSource[0] != '\0'); | |||
CARLA_SAFE_ASSERT_CONTINUE(connTarget != nullptr && connTarget[0] != '\0'); | |||
CARLA_SAFE_ASSERT_CONTINUE(connSource != nullptr && connSource[0] != '\0'); | |||
CARLA_SAFE_ASSERT_CONTINUE(connTarget != nullptr && connTarget[0] != '\0'); | |||
outPatchbay << " <Connection>\n"; | |||
outPatchbay << " <Source>" << xmlSafeString(connSource, true) << "</Source>\n"; | |||
outPatchbay << " <Target>" << xmlSafeString(connTarget, true) << "</Target>\n"; | |||
outPatchbay << " </Connection>\n"; | |||
outPatchbay << " <Connection>\n"; | |||
outPatchbay << " <Source>" << xmlSafeString(connSource, true) << "</Source>\n"; | |||
outPatchbay << " <Target>" << xmlSafeString(connTarget, true) << "</Target>\n"; | |||
outPatchbay << " </Connection>\n"; | |||
} | |||
} | |||
if (patchbayPos != nullptr && posCount != 0) | |||
{ | |||
outPatchbay << " <Positions>\n"; | |||
for (uint i=0; i<posCount; ++i) | |||
{ | |||
const PatchbayPosition& ppos(patchbayPos[i]); | |||
CARLA_SAFE_ASSERT_CONTINUE(ppos.name != nullptr && ppos.name[0] != '\0'); | |||
outPatchbay << " <Position x1=\"" << ppos.x1 << "\" y1=\"" << ppos.y1; | |||
if (ppos.x2 != 0 || ppos.y2 != 0) | |||
outPatchbay << "\" x2=\"" << ppos.x2 << "\" y2=\"" << ppos.y2; | |||
outPatchbay << "\">\n"; | |||
outPatchbay << " <Name>" << xmlSafeString(ppos.name, true) << "</Name>\n"; | |||
outPatchbay << " </Position>\n"; | |||
if (ppos.dealloc) | |||
delete[] ppos.name; | |||
} | |||
outPatchbay << " </Positions>\n"; | |||
} | |||
outPatchbay << " </Patchbay>\n"; | |||
outStream << outPatchbay; | |||
delete[] patchbayPos; | |||
} | |||
} | |||
// if we're running inside some session-manager (and using JACK), let them handle the connections | |||
bool saveExternalConnections; | |||
bool saveExternalConnections, saveExternalPositions = true; | |||
/**/ if (isPlugin) | |||
{ | |||
saveExternalConnections = false; | |||
saveExternalPositions = false; | |||
} | |||
else if (std::strcmp(getCurrentDriverName(), "JACK") != 0) | |||
{ | |||
saveExternalConnections = true; | |||
} | |||
else if (std::getenv("CARLA_DONT_MANAGE_CONNECTIONS") != nullptr) | |||
{ | |||
saveExternalConnections = false; | |||
} | |||
else if (std::getenv("LADISH_APP_NAME") != nullptr) | |||
{ | |||
saveExternalConnections = false; | |||
} | |||
else if (std::getenv("NSM_URL") != nullptr) | |||
{ | |||
saveExternalConnections = false; | |||
} | |||
else | |||
{ | |||
saveExternalConnections = true; | |||
} | |||
if (saveExternalConnections) | |||
if (saveExternalConnections || saveExternalPositions) | |||
{ | |||
if (const char* const* const patchbayConns = getPatchbayConnections(true)) | |||
uint posCount = 0; | |||
const char* const* const patchbayConns = saveExternalConnections | |||
? getPatchbayConnections(true) | |||
: nullptr; | |||
const PatchbayPosition* const patchbayPos = saveExternalPositions | |||
? getPatchbayPositions(true, posCount) | |||
: nullptr; | |||
if (patchbayConns != nullptr || patchbayPos != nullptr) | |||
{ | |||
MemoryOutputStream outPatchbay(2048); | |||
outPatchbay << "\n <ExternalPatchbay>\n"; | |||
for (int i=0; patchbayConns[i] != nullptr && patchbayConns[i+1] != nullptr; ++i, ++i ) | |||
if (patchbayConns != nullptr) | |||
{ | |||
const char* const connSource(patchbayConns[i]); | |||
const char* const connTarget(patchbayConns[i+1]); | |||
for (int i=0; patchbayConns[i] != nullptr && patchbayConns[i+1] != nullptr; ++i, ++i ) | |||
{ | |||
const char* const connSource(patchbayConns[i]); | |||
const char* const connTarget(patchbayConns[i+1]); | |||
CARLA_SAFE_ASSERT_CONTINUE(connSource != nullptr && connSource[0] != '\0'); | |||
CARLA_SAFE_ASSERT_CONTINUE(connTarget != nullptr && connTarget[0] != '\0'); | |||
CARLA_SAFE_ASSERT_CONTINUE(connSource != nullptr && connSource[0] != '\0'); | |||
CARLA_SAFE_ASSERT_CONTINUE(connTarget != nullptr && connTarget[0] != '\0'); | |||
outPatchbay << " <Connection>\n"; | |||
outPatchbay << " <Source>" << xmlSafeString(connSource, true) << "</Source>\n"; | |||
outPatchbay << " <Target>" << xmlSafeString(connTarget, true) << "</Target>\n"; | |||
outPatchbay << " </Connection>\n"; | |||
outPatchbay << " <Connection>\n"; | |||
outPatchbay << " <Source>" << xmlSafeString(connSource, true) << "</Source>\n"; | |||
outPatchbay << " <Target>" << xmlSafeString(connTarget, true) << "</Target>\n"; | |||
outPatchbay << " </Connection>\n"; | |||
} | |||
} | |||
if (patchbayPos != nullptr && posCount != 0) | |||
{ | |||
outPatchbay << " <Positions>\n"; | |||
for (uint i=0; i<posCount; ++i) | |||
{ | |||
const PatchbayPosition& ppos(patchbayPos[i]); | |||
CARLA_SAFE_ASSERT_CONTINUE(ppos.name != nullptr && ppos.name[0] != '\0'); | |||
outPatchbay << " <Position x1=\"" << ppos.x1 << "\" y1=\"" << ppos.y1; | |||
if (ppos.x2 != 0 || ppos.y2 != 0) | |||
outPatchbay << "\" x2=\"" << ppos.x2 << "\" y2=\"" << ppos.y2; | |||
outPatchbay << "\">\n"; | |||
outPatchbay << " <Name>" << xmlSafeString(ppos.name, true) << "</Name>\n"; | |||
outPatchbay << " </Position>\n"; | |||
if (ppos.dealloc) | |||
delete[] ppos.name; | |||
} | |||
outPatchbay << " </Positions>\n"; | |||
} | |||
outPatchbay << " </ExternalPatchbay>\n"; | |||
@@ -2843,8 +2925,9 @@ bool CarlaEngine::loadProjectInternal(water::XmlDocument& xmlDoc) | |||
// plus external connections too | |||
if (loadExternalConnections) | |||
{ | |||
const bool loadingAsExternal = pData->options.processMode == ENGINE_PROCESS_MODE_PATCHBAY && | |||
hasInternalConnections; | |||
bool loadingAsExternal = hasInternalConnections && | |||
(pData->options.processMode == ENGINE_PROCESS_MODE_CONTINUOUS_RACK || | |||
pData->options.processMode == ENGINE_PROCESS_MODE_PATCHBAY); | |||
for (XmlElement* elem = xmlElement->getFirstChildElement(); elem != nullptr; elem = elem->getNextElement()) | |||
{ | |||
@@ -2857,7 +2940,11 @@ bool CarlaEngine::loadProjectInternal(water::XmlDocument& xmlDoc) | |||
continue; | |||
} | |||
// or load external patchbay connections | |||
else if (tagName != "ExternalPatchbay") | |||
else if (tagName == "ExternalPatchbay") | |||
{ | |||
loadingAsExternal = true; | |||
} | |||
else | |||
{ | |||
continue; | |||
} | |||
@@ -2891,6 +2978,82 @@ bool CarlaEngine::loadProjectInternal(water::XmlDocument& xmlDoc) | |||
break; | |||
} | |||
} | |||
// finally, we handle positions | |||
if (XmlElement* const elemPatchbay = xmlElement->getChildByName("Patchbay")) | |||
{ | |||
if (XmlElement* const elemPositions = elemPatchbay->getChildByName("Positions")) | |||
{ | |||
String name; | |||
PatchbayPosition ppos = { nullptr, 0, 0, 0, 0, false }; | |||
for (XmlElement* patchElem = elemPositions->getFirstChildElement(); patchElem != nullptr; patchElem = patchElem->getNextElement()) | |||
{ | |||
const String& patchTag(patchElem->getTagName()); | |||
if (patchTag != "Position") | |||
continue; | |||
XmlElement* const patchName = patchElem->getChildByName("Name"); | |||
CARLA_SAFE_ASSERT_CONTINUE(patchName != nullptr); | |||
const String nameText(patchName->getAllSubText().trim()); | |||
name = xmlSafeString(nameText, false); | |||
ppos.name = name.toRawUTF8(); | |||
ppos.x1 = patchElem->getIntAttribute("x1"); | |||
ppos.y1 = patchElem->getIntAttribute("y1"); | |||
ppos.x2 = patchElem->getIntAttribute("x2"); | |||
ppos.y2 = patchElem->getIntAttribute("y2"); | |||
if (name.isNotEmpty()) | |||
restorePatchbayGroupPosition(false, ppos); | |||
} | |||
callback(true, true, ENGINE_CALLBACK_IDLE, 0, 0, 0, 0, 0.0f, nullptr); | |||
if (pData->aboutToClose) | |||
return true; | |||
if (pData->actionCanceled) | |||
{ | |||
setLastError("Project load canceled"); | |||
return false; | |||
} | |||
} | |||
} | |||
if (XmlElement* const elemPatchbay = xmlElement->getChildByName("ExternalPatchbay")) | |||
{ | |||
if (XmlElement* const elemPositions = elemPatchbay->getChildByName("Positions")) | |||
{ | |||
String name; | |||
PatchbayPosition ppos = { nullptr, 0, 0, 0, 0, false }; | |||
for (XmlElement* patchElem = elemPositions->getFirstChildElement(); patchElem != nullptr; patchElem = patchElem->getNextElement()) | |||
{ | |||
const String& patchTag(patchElem->getTagName()); | |||
if (patchTag != "Position") | |||
continue; | |||
XmlElement* const patchName = patchElem->getChildByName("Name"); | |||
CARLA_SAFE_ASSERT_CONTINUE(patchName != nullptr); | |||
const String nameText(patchName->getAllSubText().trim()); | |||
name = xmlSafeString(nameText, false); | |||
ppos.name = name.toRawUTF8(); | |||
ppos.x1 = patchElem->getIntAttribute("x1"); | |||
ppos.y1 = patchElem->getIntAttribute("y1"); | |||
ppos.x2 = patchElem->getIntAttribute("x2"); | |||
ppos.y2 = patchElem->getIntAttribute("y2"); | |||
if (name.isNotEmpty()) | |||
restorePatchbayGroupPosition(true, ppos); | |||
} | |||
} | |||
} | |||
#endif | |||
if (pData->options.resetXruns) | |||
@@ -134,8 +134,12 @@ ExternalGraph::ExternalGraph(CarlaEngine* const engine) noexcept | |||
: connections(), | |||
audioPorts(), | |||
midiPorts(), | |||
positions(), | |||
retCon(), | |||
kEngine(engine) {} | |||
kEngine(engine) | |||
{ | |||
carla_zeroStruct(positions); | |||
} | |||
void ExternalGraph::clear() noexcept | |||
{ | |||
@@ -313,11 +317,24 @@ bool ExternalGraph::disconnect(const bool sendHost, const bool sendOSC, | |||
return false; | |||
} | |||
void ExternalGraph::setGroupPos(const bool sendHost, const bool sendOSC, | |||
const uint groupId, const int x1, const int y1, const int x2, const int y2) | |||
{ | |||
CARLA_SAFE_ASSERT_UINT_RETURN(groupId >= kExternalGraphGroupCarla && groupId < kExternalGraphGroupMax, groupId,); | |||
positions[groupId] = { true, x1, y1, x2, y2 }; | |||
kEngine->callback(sendHost, sendOSC, | |||
ENGINE_CALLBACK_PATCHBAY_CLIENT_POSITION_CHANGED, | |||
groupId, x1, y1, x2, static_cast<float>(y2), | |||
nullptr); | |||
} | |||
void ExternalGraph::refresh(const bool sendHost, const bool sendOSC, const char* const deviceName) | |||
{ | |||
CARLA_SAFE_ASSERT_RETURN(deviceName != nullptr,); | |||
const bool isRack(kEngine->getOptions().processMode == ENGINE_PROCESS_MODE_CONTINUOUS_RACK); | |||
const bool isRack = kEngine->getOptions().processMode == ENGINE_PROCESS_MODE_CONTINUOUS_RACK; | |||
// Main | |||
{ | |||
@@ -591,6 +608,39 @@ const char* const* ExternalGraph::getConnections() const noexcept | |||
return retCon; | |||
} | |||
bool ExternalGraph::getGroupFromName(const char* groupName, uint& groupId) const noexcept | |||
{ | |||
CARLA_SAFE_ASSERT_RETURN(groupName != nullptr && groupName[0] != '\0', false); | |||
if (std::strcmp(groupName, "Carla") == 0) | |||
{ | |||
groupId = kExternalGraphGroupCarla; | |||
return true; | |||
} | |||
if (std::strcmp(groupName, "AudioIn") == 0) | |||
{ | |||
groupId = kExternalGraphGroupAudioIn; | |||
return true; | |||
} | |||
if (std::strcmp(groupName, "AudioOut") == 0) | |||
{ | |||
groupId = kExternalGraphGroupAudioOut; | |||
return true; | |||
} | |||
if (std::strcmp(groupName, "MidiIn") == 0) | |||
{ | |||
groupId = kExternalGraphGroupMidiIn; | |||
return true; | |||
} | |||
if (std::strcmp(groupName, "MidiOut") == 0) | |||
{ | |||
groupId = kExternalGraphGroupMidiOut; | |||
return true; | |||
} | |||
return false; | |||
} | |||
bool ExternalGraph::getGroupAndPortIdFromFullName(const char* const fullPortName, uint& groupId, uint& portId) const noexcept | |||
{ | |||
CARLA_SAFE_ASSERT_RETURN(fullPortName != nullptr && fullPortName[0] != '\0', false); | |||
@@ -1230,16 +1280,19 @@ const String getProcessorFullPortName(AudioProcessor* const proc, const uint32_t | |||
static inline | |||
void addNodeToPatchbay(const bool sendHost, const bool sendOSC, CarlaEngine* const engine, | |||
const uint32_t groupId, const int clientId, const AudioProcessor* const proc) | |||
AudioProcessorGraph::Node* const node, const int pluginId, const AudioProcessor* const proc) | |||
{ | |||
CARLA_SAFE_ASSERT_RETURN(engine != nullptr,); | |||
CARLA_SAFE_ASSERT_RETURN(node != nullptr,); | |||
CARLA_SAFE_ASSERT_RETURN(proc != nullptr,); | |||
const uint groupId = node->nodeId; | |||
engine->callback(sendHost, sendOSC, | |||
ENGINE_CALLBACK_PATCHBAY_CLIENT_ADDED, | |||
groupId, | |||
clientId >= 0 ? PATCHBAY_ICON_PLUGIN : PATCHBAY_ICON_HARDWARE, | |||
clientId, | |||
pluginId >= 0 ? PATCHBAY_ICON_PLUGIN : PATCHBAY_ICON_HARDWARE, | |||
pluginId, | |||
0, 0.0f, | |||
proc->getName().toRawUTF8()); | |||
@@ -1308,6 +1361,18 @@ void addNodeToPatchbay(const bool sendHost, const bool sendOSC, CarlaEngine* con | |||
0, 0.0f, | |||
proc->getOutputChannelName(AudioProcessor::ChannelTypeMIDI, i).toRawUTF8()); | |||
} | |||
if (node->properties.contains("x1")) | |||
{ | |||
engine->callback(sendHost, sendOSC, | |||
ENGINE_CALLBACK_PATCHBAY_CLIENT_POSITION_CHANGED, | |||
groupId, | |||
node->properties.getWithDefault("x1", 0), | |||
node->properties.getWithDefault("y1", 0), | |||
node->properties.getWithDefault("x2", 0), | |||
static_cast<float>(node->properties.getWithDefault("y2", 0)), | |||
nullptr); | |||
} | |||
} | |||
static inline | |||
@@ -1813,7 +1878,7 @@ void PatchbayGraph::addPlugin(CarlaPlugin* const plugin) | |||
node->properties.set("isPlugin", true); | |||
node->properties.set("pluginId", static_cast<int>(plugin->getId())); | |||
addNodeToPatchbay(sendHost, sendOSC, kEngine, node->nodeId, static_cast<int>(plugin->getId()), instance); | |||
addNodeToPatchbay(sendHost, sendOSC, kEngine, node, static_cast<int>(plugin->getId()), instance); | |||
} | |||
void PatchbayGraph::replacePlugin(CarlaPlugin* const oldPlugin, CarlaPlugin* const newPlugin) | |||
@@ -1845,7 +1910,7 @@ void PatchbayGraph::replacePlugin(CarlaPlugin* const oldPlugin, CarlaPlugin* con | |||
node->properties.set("isPlugin", true); | |||
node->properties.set("pluginId", static_cast<int>(newPlugin->getId())); | |||
addNodeToPatchbay(sendHost, sendOSC, kEngine, node->nodeId, static_cast<int>(newPlugin->getId()), instance); | |||
addNodeToPatchbay(sendHost, sendOSC, kEngine, node, static_cast<int>(newPlugin->getId()), instance); | |||
} | |||
void PatchbayGraph::renamePlugin(CarlaPlugin* const plugin, const char* const newName) | |||
@@ -2093,6 +2158,26 @@ void PatchbayGraph::disconnectInternalGroup(const uint groupId) noexcept | |||
} | |||
} | |||
void PatchbayGraph::setGroupPos(const bool sendHost, const bool sendOSC, const bool external, | |||
uint groupId, int x1, int y1, int x2, int y2) | |||
{ | |||
if (external) | |||
return extGraph.setGroupPos(sendHost, sendOSC, groupId, x1, y1, x2, y2); | |||
AudioProcessorGraph::Node* const node(graph.getNodeForId(groupId)); | |||
CARLA_SAFE_ASSERT_RETURN(node != nullptr,); | |||
node->properties.set("x1", x1); | |||
node->properties.set("y1", y1); | |||
node->properties.set("x2", x2); | |||
node->properties.set("y2", y2); | |||
kEngine->callback(sendHost, sendOSC, | |||
ENGINE_CALLBACK_PATCHBAY_CLIENT_POSITION_CHANGED, | |||
groupId, x1, y1, x2, static_cast<float>(y2), | |||
nullptr); | |||
} | |||
void PatchbayGraph::refresh(const bool sendHost, const bool sendOSC, const bool external, const char* const deviceName) | |||
{ | |||
if (external) | |||
@@ -2111,13 +2196,13 @@ void PatchbayGraph::refresh(const bool sendHost, const bool sendOSC, const bool | |||
AudioProcessor* const proc(node->getProcessor()); | |||
CARLA_SAFE_ASSERT_CONTINUE(proc != nullptr); | |||
int clientId = -1; | |||
int pluginId = -1; | |||
// plugin node | |||
if (node->properties.getWithDefault("isPlugin", false) == water::var(true)) | |||
clientId = node->properties.getWithDefault("pluginId", -1); | |||
pluginId = node->properties.getWithDefault("pluginId", -1); | |||
addNodeToPatchbay(sendHost, sendOSC, kEngine, node->nodeId, clientId, proc); | |||
addNodeToPatchbay(sendHost, sendOSC, kEngine, node, pluginId, proc); | |||
} | |||
char strBuf[STR_MAX+1]; | |||
@@ -2212,6 +2297,115 @@ const char* const* PatchbayGraph::getConnections(const bool external) const | |||
return retCon; | |||
} | |||
const CarlaEngine::PatchbayPosition* PatchbayGraph::getPositions(bool external, uint& count) const | |||
{ | |||
CarlaEngine::PatchbayPosition* ret; | |||
if (external) | |||
{ | |||
try { | |||
ret = new CarlaEngine::PatchbayPosition[kExternalGraphGroupMax]; | |||
} CARLA_SAFE_EXCEPTION_RETURN("new CarlaEngine::PatchbayPosition", nullptr); | |||
count = 0; | |||
for (uint i=kExternalGraphGroupCarla; i<kExternalGraphGroupMax; ++i) | |||
{ | |||
const PatchbayPosition& eppos(extGraph.positions[i]); | |||
if (! eppos.active) | |||
continue; | |||
CarlaEngine::PatchbayPosition& ppos(ret[count++]); | |||
switch (i) | |||
{ | |||
case kExternalGraphGroupCarla: | |||
ppos.name = kEngine->getName(); | |||
break; | |||
case kExternalGraphGroupAudioIn: | |||
ppos.name = "Capture"; | |||
break; | |||
case kExternalGraphGroupAudioOut: | |||
ppos.name = "Playback"; | |||
break; | |||
case kExternalGraphGroupMidiIn: | |||
ppos.name = "Readable MIDI ports"; | |||
break; | |||
case kExternalGraphGroupMidiOut: | |||
ppos.name = "Writable MIDI ports"; | |||
break; | |||
} | |||
ppos.dealloc = false; | |||
ppos.x1 = eppos.x1; | |||
ppos.y1 = eppos.y1; | |||
ppos.x2 = eppos.x2; | |||
ppos.y2 = eppos.y2; | |||
} | |||
return ret; | |||
} | |||
else | |||
{ | |||
const int numNodes = graph.getNumNodes(); | |||
CARLA_SAFE_ASSERT_RETURN(numNodes > 0, nullptr); | |||
try { | |||
ret = new CarlaEngine::PatchbayPosition[numNodes]; | |||
} CARLA_SAFE_EXCEPTION_RETURN("new CarlaEngine::PatchbayPosition", nullptr); | |||
count = 0; | |||
for (int i=numNodes; --i >= 0;) | |||
{ | |||
AudioProcessorGraph::Node* const node(graph.getNode(i)); | |||
CARLA_SAFE_ASSERT_CONTINUE(node != nullptr); | |||
if (! node->properties.contains("x1")) | |||
continue; | |||
AudioProcessor* const proc(node->getProcessor()); | |||
CARLA_SAFE_ASSERT_CONTINUE(proc != nullptr); | |||
CarlaEngine::PatchbayPosition& ppos(ret[count++]); | |||
ppos.name = carla_strdup(proc->getName().toRawUTF8()); | |||
ppos.dealloc = true; | |||
ppos.x1 = node->properties.getWithDefault("x1", 0); | |||
ppos.y1 = node->properties.getWithDefault("y1", 0); | |||
ppos.x2 = node->properties.getWithDefault("x2", 0); | |||
ppos.y2 = node->properties.getWithDefault("y2", 0); | |||
} | |||
return ret; | |||
} | |||
} | |||
bool PatchbayGraph::getGroupFromName(bool external, const char* groupName, uint& groupId) const | |||
{ | |||
if (external) | |||
return extGraph.getGroupFromName(groupName, groupId); | |||
for (int i=0, count=graph.getNumNodes(); i<count; ++i) | |||
{ | |||
AudioProcessorGraph::Node* const node(graph.getNode(i)); | |||
CARLA_SAFE_ASSERT_CONTINUE(node != nullptr); | |||
AudioProcessor* const proc(node->getProcessor()); | |||
CARLA_SAFE_ASSERT_CONTINUE(proc != nullptr); | |||
if (proc->getName() != groupName) | |||
continue; | |||
groupId = node->nodeId; | |||
return true; | |||
} | |||
return false; | |||
} | |||
bool PatchbayGraph::getGroupAndPortIdFromFullName(const bool external, const char* const fullPortName, uint& groupId, uint& portId) const | |||
{ | |||
if (external) | |||
@@ -2607,8 +2801,6 @@ bool CarlaEngine::patchbayConnect(const bool external, | |||
return graph->connect(external, groupA, portA, groupB, portB); | |||
} | |||
return false; | |||
} | |||
bool CarlaEngine::patchbayDisconnect(const bool external, const uint connectionId) | |||
@@ -2631,8 +2823,28 @@ bool CarlaEngine::patchbayDisconnect(const bool external, const uint connectionI | |||
return graph->disconnect(external, connectionId); | |||
} | |||
} | |||
return false; | |||
bool CarlaEngine::patchbaySetGroupPos(const bool sendHost, const bool sendOSC, const bool external, | |||
const uint groupId, const int x1, const int y1, const int x2, const int y2) | |||
{ | |||
CARLA_SAFE_ASSERT_RETURN(pData->options.processMode == ENGINE_PROCESS_MODE_CONTINUOUS_RACK || pData->options.processMode == ENGINE_PROCESS_MODE_PATCHBAY, false); | |||
CARLA_SAFE_ASSERT_RETURN(pData->graph.isReady(), false); | |||
carla_debug("CarlaEngine::patchbaySetGroupPos(%u, %i, %i, %i, %i)", groupId, x1, y1, x2, y2); | |||
if (pData->options.processMode == ENGINE_PROCESS_MODE_CONTINUOUS_RACK) | |||
{ | |||
// we don't bother to save position in this case, there is only midi in/out | |||
return true; | |||
} | |||
else | |||
{ | |||
PatchbayGraph* const graph = pData->graph.getPatchbayGraph(); | |||
CARLA_SAFE_ASSERT_RETURN(graph != nullptr, false); | |||
graph->setGroupPos(sendHost, sendOSC, external, groupId, x1, y1, x2, y2); | |||
return true; | |||
} | |||
} | |||
bool CarlaEngine::patchbayRefresh(const bool sendHost, const bool sendOSC, const bool external) | |||
@@ -2687,6 +2899,22 @@ const char* const* CarlaEngine::getPatchbayConnections(const bool external) cons | |||
return nullptr; | |||
} | |||
const CarlaEngine::PatchbayPosition* CarlaEngine::getPatchbayPositions(bool external, uint& count) const | |||
{ | |||
CARLA_SAFE_ASSERT_RETURN(pData->graph.isReady(), nullptr); | |||
carla_debug("CarlaEngine::getPatchbayPositions(%s)", bool2str(external)); | |||
if (pData->options.processMode == ENGINE_PROCESS_MODE_PATCHBAY) | |||
{ | |||
PatchbayGraph* const graph = pData->graph.getPatchbayGraph(); | |||
CARLA_SAFE_ASSERT_RETURN(graph != nullptr, nullptr); | |||
return graph->getPositions(external, count); | |||
} | |||
return nullptr; | |||
} | |||
void CarlaEngine::restorePatchbayConnection(const bool external, | |||
const char* const sourcePort, | |||
const char* const targetPort) | |||
@@ -2726,6 +2954,23 @@ void CarlaEngine::restorePatchbayConnection(const bool external, | |||
} | |||
} | |||
void CarlaEngine::restorePatchbayGroupPosition(const bool external, const PatchbayPosition& ppos) | |||
{ | |||
CARLA_SAFE_ASSERT_RETURN(pData->graph.isReady(),); | |||
CARLA_SAFE_ASSERT_RETURN(ppos.name != nullptr && ppos.name[0] != '\0',); | |||
if (pData->options.processMode == ENGINE_PROCESS_MODE_PATCHBAY) | |||
{ | |||
PatchbayGraph* const graph = pData->graph.getPatchbayGraph(); | |||
CARLA_SAFE_ASSERT_RETURN(graph != nullptr,); | |||
uint groupId; | |||
CARLA_SAFE_ASSERT_RETURN(graph->getGroupFromName(external, ppos.name, groupId),); | |||
graph->setGroupPos(true, true, external, groupId, ppos.x1, ppos.y1, ppos.x2, ppos.y2); | |||
} | |||
} | |||
// ----------------------------------------------------------------------- | |||
bool CarlaEngine::connectExternalGraphPort(const uint connectionType, const uint portId, const char* const portName) | |||
@@ -33,6 +33,13 @@ using water::MidiBuffer; | |||
CARLA_BACKEND_START_NAMESPACE | |||
// ----------------------------------------------------------------------- | |||
struct PatchbayPosition { | |||
bool active; | |||
int x1, y1, x2, y2; | |||
}; | |||
// ----------------------------------------------------------------------- | |||
// External Graph stuff | |||
@@ -80,6 +87,7 @@ struct ExternalGraphPorts { | |||
struct ExternalGraph { | |||
PatchbayConnectionList connections; | |||
ExternalGraphPorts audioPorts, midiPorts; | |||
PatchbayPosition positions[kExternalGraphGroupMax]; | |||
mutable CharStringListPtr retCon; | |||
ExternalGraph(CarlaEngine* engine) noexcept; | |||
@@ -89,10 +97,13 @@ struct ExternalGraph { | |||
uint groupA, uint portA, uint groupB, uint portB) noexcept; | |||
bool disconnect(bool sendHost, bool sendOSC, | |||
uint connectionId) noexcept; | |||
void setGroupPos(bool sendHost, bool sendOSC, | |||
uint groupId, int x1, int y1, int x2, int y2); | |||
void refresh(bool sendHost, bool sendOSC, | |||
const char* deviceName); | |||
const char* const* getConnections() const noexcept; | |||
bool getGroupFromName(const char* groupName, uint& groupId) const noexcept; | |||
bool getGroupAndPortIdFromFullName(const char* fullPortName, uint& groupId, uint& portId) const noexcept; | |||
CarlaEngine* const kEngine; | |||
@@ -190,9 +201,12 @@ public: | |||
bool connect(bool external, uint groupA, uint portA, uint groupB, uint portB); | |||
bool disconnect(bool external, uint connectionId); | |||
void disconnectInternalGroup(uint groupId) noexcept; | |||
void setGroupPos(bool sendHost, bool sendOsc, bool external, uint groupId, int x1, int y1, int x2, int y2); | |||
void refresh(bool sendHost, bool sendOsc, bool external, const char* deviceName); | |||
const char* const* getConnections(bool external) const; | |||
const CarlaEngine::PatchbayPosition* getPositions(bool external, uint& count) const; | |||
bool getGroupFromName(bool external, const char* groupName, uint& groupId) const; | |||
bool getGroupAndPortIdFromFullName(bool external, const char* fullPortName, uint& groupId, uint& portId) const; | |||
void process(CarlaEngine::ProtectedData* data, | |||
@@ -1816,6 +1816,22 @@ bool CarlaEngineNativeUI::msgReceived(const char* const msg) noexcept | |||
ok = fEngine->patchbayDisconnect(external, connectionId); | |||
} CARLA_SAFE_EXCEPTION("patchbayDisconnect"); | |||
} | |||
else if (std::strcmp(msg, "patchbay_set_group_pos") == 0) | |||
{ | |||
bool external; | |||
uint32_t groupId; | |||
int x1, y1, x2, y2; | |||
CARLA_SAFE_ASSERT_RETURN(readNextLineAsBool(external), true); | |||
CARLA_SAFE_ASSERT_RETURN(readNextLineAsUInt(groupId), true); | |||
CARLA_SAFE_ASSERT_RETURN(readNextLineAsInt(x1), true); | |||
CARLA_SAFE_ASSERT_RETURN(readNextLineAsInt(y1), true); | |||
CARLA_SAFE_ASSERT_RETURN(readNextLineAsInt(x2), true); | |||
CARLA_SAFE_ASSERT_RETURN(readNextLineAsInt(y2), true); | |||
try { | |||
ok = fEngine->patchbaySetGroupPos(true, false, external, groupId, x1, y1, x2, y2); | |||
} CARLA_SAFE_EXCEPTION("patchbaySetGroupPos"); | |||
} | |||
else if (std::strcmp(msg, "patchbay_refresh") == 0) | |||
{ | |||
bool external; | |||
@@ -818,6 +818,14 @@ ENGINE_CALLBACK_PATCHBAY_PORT_GROUP_CHANGED = 45 | |||
# @a valueStr New mapped range as "%f:%f" syntax | |||
ENGINE_CALLBACK_PARAMETER_MAPPED_RANGE_CHANGED = 46 | |||
# A patchbay client position has changed. | |||
# @a pluginId Client Id | |||
# @a value1 X position 1 | |||
# @a value2 Y position 1 | |||
# @a value3 X position 2 | |||
# @a valuef Y position 2 | |||
ENGINE_CALLBACK_PATCHBAY_CLIENT_POSITION_CHANGED = 47 | |||
# ------------------------------------------------------------------------------------------------------------ | |||
# NSM Callback Opcode | |||
# NSM callback opcodes. | |||
@@ -1678,6 +1686,14 @@ class CarlaHostMeta(object): | |||
def patchbay_disconnect(self, external, connectionId): | |||
raise NotImplementedError | |||
# Set the position of a group. | |||
# This is purely cached and saved in the project file, Carla backend does nothing with the value. | |||
# When loading a project, callbacks are used to inform of the previously saved positions. | |||
# @see ENGINE_CALLBACK_PATCHBAY_CLIENT_POSITION_CHANGED | |||
@abstractmethod | |||
def patchbay_set_group_pos(self, external, groupId, x1, y1, x2, y2): | |||
raise NotImplementedError | |||
# Force the engine to resend all patchbay clients, ports and connections again. | |||
# @param external Wherever to show external/hardware ports instead of internal ones. | |||
# Only valid in patchbay engine mode, other modes will ignore this. | |||
@@ -2306,6 +2322,9 @@ class CarlaHostNull(CarlaHostMeta): | |||
def patchbay_disconnect(self, external, connectionId): | |||
return False | |||
def patchbay_set_group_pos(self, external, groupId, x1, y1, x2, y2): | |||
return False | |||
def patchbay_refresh(self, external): | |||
return False | |||
@@ -2624,6 +2643,9 @@ class CarlaHostDLL(CarlaHostMeta): | |||
self.lib.carla_patchbay_disconnect.argtypes = (c_void_p, c_bool, c_uint) | |||
self.lib.carla_patchbay_disconnect.restype = c_bool | |||
self.lib.carla_patchbay_set_group_pos.argtypes = (c_void_p, c_bool, c_uint, c_int, c_int, c_int, c_int) | |||
self.lib.carla_patchbay_set_group_pos.restype = c_bool | |||
self.lib.carla_patchbay_refresh.argtypes = (c_void_p, c_bool) | |||
self.lib.carla_patchbay_refresh.restype = c_bool | |||
@@ -2933,6 +2955,9 @@ class CarlaHostDLL(CarlaHostMeta): | |||
def patchbay_disconnect(self, external, connectionId): | |||
return bool(self.lib.carla_patchbay_disconnect(self.handle, external, connectionId)) | |||
def patchbay_set_group_pos(self, external, groupId, x1, y1, x2, y2): | |||
return bool(self.lib.carla_patchbay_set_group_pos(self.handle, external, groupId, x1, y1, x2, y2)) | |||
def patchbay_refresh(self, external): | |||
return bool(self.lib.carla_patchbay_refresh(self.handle, external)) | |||
@@ -3325,6 +3350,9 @@ class CarlaHostPlugin(CarlaHostMeta): | |||
def patchbay_disconnect(self, external, connectionId): | |||
return self.sendMsgAndSetError(["patchbay_disconnect", external, connectionId]) | |||
def patchbay_set_group_pos(self, external, groupId, x1, y1, x2, y2): | |||
return self.sendMsgAndSetError(["patchbay_set_group_pos", external, groupId, x1, y1, x2, y2]) | |||
def patchbay_refresh(self, external): | |||
return self.sendMsgAndSetError(["patchbay_refresh", external]) | |||
@@ -56,6 +56,7 @@ class CarlaHostSignals(QObject): | |||
PatchbayClientRemovedCallback = pyqtSignal(int) | |||
PatchbayClientRenamedCallback = pyqtSignal(int, str) | |||
PatchbayClientDataChangedCallback = pyqtSignal(int, int, int) | |||
PatchbayClientPositionChangedCallback = pyqtSignal(int, int, int, int, int) | |||
PatchbayPortAddedCallback = pyqtSignal(int, int, int, int, str) | |||
PatchbayPortRemovedCallback = pyqtSignal(int, int) | |||
PatchbayPortChangedCallback = pyqtSignal(int, int, int, int, str) | |||
@@ -505,7 +505,6 @@ class HostWindow(QMainWindow): | |||
self.ui.graphicsView.verticalScrollBar().valueChanged.connect(self.slot_verticalScrollBarChanged) | |||
self.ui.miniCanvasPreview.miniCanvasMoved.connect(self.slot_miniCanvasMoved) | |||
self.scene.scaleChanged.connect(self.slot_canvasScaleChanged) | |||
self.scene.sceneGroupMoved.connect(self.slot_canvasItemMoved) | |||
self.scene.pluginSelected.connect(self.slot_canvasPluginSelected) | |||
self.scene.selectionChanged.connect(self.slot_canvasSelectionChanged) | |||
@@ -533,6 +532,7 @@ class HostWindow(QMainWindow): | |||
host.PatchbayClientRemovedCallback.connect(self.slot_handlePatchbayClientRemovedCallback) | |||
host.PatchbayClientRenamedCallback.connect(self.slot_handlePatchbayClientRenamedCallback) | |||
host.PatchbayClientDataChangedCallback.connect(self.slot_handlePatchbayClientDataChangedCallback) | |||
host.PatchbayClientPositionChangedCallback.connect(self.slot_handlePatchbayClientPositionChangedCallback) | |||
host.PatchbayPortAddedCallback.connect(self.slot_handlePatchbayPortAddedCallback) | |||
host.PatchbayPortRemovedCallback.connect(self.slot_handlePatchbayPortRemovedCallback) | |||
host.PatchbayPortChangedCallback.connect(self.slot_handlePatchbayPortChangedCallback) | |||
@@ -713,9 +713,6 @@ class HostWindow(QMainWindow): | |||
# -------------------------------------------------------------------------------------------------------- | |||
# Files | |||
def makeExtraFilename(self): | |||
return self.fProjectFilename.rsplit(".",1)[0]+".json" | |||
def loadProjectNow(self): | |||
if not self.fProjectFilename: | |||
return qCritical("ERROR: loading project without filename set") | |||
@@ -748,14 +745,6 @@ class HostWindow(QMainWindow): | |||
QMessageBox.Ok, QMessageBox.Ok) | |||
return | |||
if not self.fWithCanvas: | |||
return | |||
with open(self.makeExtraFilename(), 'w') as fh: | |||
json.dump({ | |||
'canvas': patchcanvas.saveGroupPositions(), | |||
}, fh) | |||
def projectLoadingStarted(self): | |||
self.ui.rack.setEnabled(False) | |||
self.ui.graphicsView.setEnabled(False) | |||
@@ -769,7 +758,7 @@ class HostWindow(QMainWindow): | |||
QTimer.singleShot(1000, self.slot_canvasRefresh) | |||
extrafile = self.makeExtraFilename() | |||
extrafile = self.fProjectFilename.rsplit(".",1)[0]+".json" | |||
if not os.path.exists(extrafile): | |||
return | |||
@@ -1431,7 +1420,7 @@ class HostWindow(QMainWindow): | |||
pFeatures.group_rename = False | |||
pFeatures.port_info = False | |||
pFeatures.port_rename = False | |||
pFeatures.handle_group_pos = True | |||
pFeatures.handle_group_pos = False | |||
patchcanvas.setOptions(pOptions) | |||
patchcanvas.setFeatures(pFeatures) | |||
@@ -1567,10 +1556,6 @@ class HostWindow(QMainWindow): | |||
# -------------------------------------------------------------------------------------------------------- | |||
# Canvas (canvas callbacks) | |||
@pyqtSlot(int, int, QPointF) | |||
def slot_canvasItemMoved(self, group_id, split_mode, pos): | |||
self.updateMiniCanvasLater() | |||
@pyqtSlot() | |||
def slot_canvasSelectionChanged(self): | |||
self.updateMiniCanvasLater() | |||
@@ -1668,6 +1653,10 @@ class HostWindow(QMainWindow): | |||
patchcanvas.setGroupAsPlugin(clientId, pluginId, hasCustomUI, hasInlineDisplay) | |||
@pyqtSlot(int, int, int, int, int) | |||
def slot_handlePatchbayClientPositionChangedCallback(self, clientId, x1, y1, x2, y2): | |||
patchcanvas.setGroupPosFull(clientId, x1, y1, x2, y2) | |||
@pyqtSlot(int, int, int, int, str) | |||
def slot_handlePatchbayPortAddedCallback(self, clientId, portId, portFlags, portGroupId, portName): | |||
if portFlags & PATCHBAY_PORT_IS_INPUT: | |||
@@ -2720,6 +2709,9 @@ class HostWindow(QMainWindow): | |||
def canvasCallback(action, value1, value2, valueStr): | |||
host = gCarla.gui.host | |||
if gCarla.gui.fCustomStopAction == HostWindow.CUSTOM_ACTION_APP_CLOSE: | |||
return | |||
if action == patchcanvas.ACTION_GROUP_INFO: | |||
pass | |||
@@ -2736,6 +2728,14 @@ def canvasCallback(action, value1, value2, valueStr): | |||
patchcanvas.joinGroup(groupId) | |||
gCarla.gui.updateMiniCanvasLater() | |||
elif action == patchcanvas.ACTION_GROUP_POSITION: | |||
if gCarla.gui.fIsProjectLoading or not host.is_engine_running(): | |||
return | |||
groupId = value1 | |||
x1, y1, x2, y2 = tuple(int(i) for i in valueStr.split(":")) | |||
host.patchbay_set_group_pos(gCarla.gui.fExternalPatchbay, groupId, x1, y1, x2, y2) | |||
gCarla.gui.updateMiniCanvasLater() | |||
elif action == patchcanvas.ACTION_PORT_INFO: | |||
pass | |||
@@ -2743,7 +2743,7 @@ def canvasCallback(action, value1, value2, valueStr): | |||
pass | |||
elif action == patchcanvas.ACTION_PORTS_CONNECT: | |||
gOut, pOut, gIn, pIn = [int(i) for i in valueStr.split(":")] | |||
gOut, pOut, gIn, pIn = tuple(int(i) for i in valueStr.split(":")) | |||
if not host.patchbay_connect(gCarla.gui.fExternalPatchbay, gOut, pOut, gIn, pIn): | |||
print("Connection failed:", host.get_last_error()) | |||
@@ -2875,6 +2875,8 @@ def engineCallback(host, action, pluginId, value1, value2, value3, valuef, value | |||
host.PatchbayClientRenamedCallback.emit(pluginId, valueStr) | |||
elif action == ENGINE_CALLBACK_PATCHBAY_CLIENT_DATA_CHANGED: | |||
host.PatchbayClientDataChangedCallback.emit(pluginId, value1, value2) | |||
elif action == ENGINE_CALLBACK_PATCHBAY_CLIENT_POSITION_CHANGED: | |||
host.PatchbayClientPositionChangedCallback.emit(pluginId, value1, value2, value3, int(round(valuef))) | |||
elif action == ENGINE_CALLBACK_PATCHBAY_PORT_ADDED: | |||
host.PatchbayPortAddedCallback.emit(pluginId, value1, value2, value3, valueStr) | |||
elif action == ENGINE_CALLBACK_PATCHBAY_PORT_REMOVED: | |||
@@ -49,18 +49,19 @@ ACTION_GROUP_INFO = 0 # group_id, N, N | |||
ACTION_GROUP_RENAME = 1 # group_id, N, N | |||
ACTION_GROUP_SPLIT = 2 # group_id, N, N | |||
ACTION_GROUP_JOIN = 3 # group_id, N, N | |||
ACTION_PORT_INFO = 4 # group_id, port_id, N | |||
ACTION_PORT_RENAME = 5 # group_id, port_id, N | |||
ACTION_PORTS_CONNECT = 6 # N, N, "outG:outP:inG:inP" | |||
ACTION_PORTS_DISCONNECT = 7 # conn_id, N, N | |||
ACTION_PLUGIN_CLONE = 8 # plugin_id, N, N | |||
ACTION_PLUGIN_EDIT = 9 # plugin_id, N, N | |||
ACTION_PLUGIN_RENAME = 10 # plugin_id, N, N | |||
ACTION_PLUGIN_REPLACE = 11 # plugin_id, N, N | |||
ACTION_PLUGIN_REMOVE = 12 # plugin_id, N, N | |||
ACTION_PLUGIN_SHOW_UI = 13 # plugin_id, N, N | |||
ACTION_BG_RIGHT_CLICK = 14 # N, N, N | |||
ACTION_INLINE_DISPLAY = 15 # plugin_id, N, N | |||
ACTION_GROUP_POSITION = 4 # group_id, N, N, "x1:y1:x2:y2" | |||
ACTION_PORT_INFO = 5 # group_id, port_id, N | |||
ACTION_PORT_RENAME = 6 # group_id, port_id, N | |||
ACTION_PORTS_CONNECT = 7 # N, N, "outG:outP:inG:inP" | |||
ACTION_PORTS_DISCONNECT = 8 # conn_id, N, N | |||
ACTION_PLUGIN_CLONE = 9 # plugin_id, N, N | |||
ACTION_PLUGIN_EDIT = 10 # plugin_id, N, N | |||
ACTION_PLUGIN_RENAME = 11 # plugin_id, N, N | |||
ACTION_PLUGIN_REPLACE = 12 # plugin_id, N, N | |||
ACTION_PLUGIN_REMOVE = 13 # plugin_id, N, N | |||
ACTION_PLUGIN_SHOW_UI = 14 # plugin_id, N, N | |||
ACTION_BG_RIGHT_CLICK = 15 # N, N, N | |||
ACTION_INLINE_DISPLAY = 16 # plugin_id, N, N | |||
# Icon | |||
ICON_APPLICATION = 0 | |||
@@ -134,6 +135,7 @@ class Canvas(object): | |||
self.connection_list = [] | |||
self.animation_list = [] | |||
self.group_plugin_map = {} | |||
self.old_group_pos = {} | |||
self.callback = self.callback | |||
self.debug = False | |||
@@ -22,9 +22,9 @@ | |||
from sip import voidptr | |||
from struct import pack | |||
from PyQt5.QtCore import qCritical, Qt, QPointF, QRectF, QTimer | |||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, qCritical, Qt, QPointF, QRectF, QTimer | |||
from PyQt5.QtGui import QCursor, QFont, QFontMetrics, QImage, QLinearGradient, QPainter, QPen | |||
from PyQt5.QtWidgets import QGraphicsItem, QMenu | |||
from PyQt5.QtWidgets import QGraphicsItem, QGraphicsObject, QMenu | |||
# ------------------------------------------------------------------------------------------------------------ | |||
# Imports (Custom) | |||
@@ -75,13 +75,17 @@ class cb_line_t(object): | |||
# ------------------------------------------------------------------------------------------------------------ | |||
class CanvasBox(QGraphicsItem): | |||
class CanvasBox(QGraphicsObject): | |||
# signals | |||
positionChanged = pyqtSignal(int, bool, int, int) | |||
# enums | |||
INLINE_DISPLAY_DISABLED = 0 | |||
INLINE_DISPLAY_ENABLED = 1 | |||
INLINE_DISPLAY_CACHED = 2 | |||
def __init__(self, group_id, group_name, icon, parent=None): | |||
QGraphicsItem.__init__(self) | |||
QGraphicsObject.__init__(self) | |||
self.setParentItem(parent) | |||
# Save Variables, useful for later | |||
@@ -109,6 +113,7 @@ class CanvasBox(QGraphicsItem): | |||
self.m_inline_data = None | |||
self.m_inline_image = None | |||
self.m_inline_scaling = 1.0 | |||
self.m_will_signal_pos_change = False | |||
self.m_port_list_ids = [] | |||
self.m_connection_lines = [] | |||
@@ -153,6 +158,10 @@ class CanvasBox(QGraphicsItem): | |||
self.updatePositions() | |||
self.visibleChanged.connect(self.slot_signalPositionChangedLater) | |||
self.xChanged.connect(self.slot_signalPositionChangedLater) | |||
self.yChanged.connect(self.slot_signalPositionChangedLater) | |||
canvas.scene.addItem(self) | |||
QTimer.singleShot(0, self.fixPos) | |||
@@ -270,21 +279,31 @@ class CanvasBox(QGraphicsItem): | |||
return | |||
qCritical("PatchCanvas::CanvasBox.removeLineFromGroup(%i) - unable to find line to remove" % connection_id) | |||
def checkItemPos(self): | |||
if not canvas.size_rect.isNull(): | |||
pos = self.scenePos() | |||
if not (canvas.size_rect.contains(pos) and | |||
canvas.size_rect.contains(pos + QPointF(self.p_width, self.p_height))): | |||
if pos.x() < canvas.size_rect.x(): | |||
self.setPos(canvas.size_rect.x(), pos.y()) | |||
elif pos.x() + self.p_width > canvas.size_rect.width(): | |||
self.setPos(canvas.size_rect.width() - self.p_width, pos.y()) | |||
pos = self.scenePos() | |||
if pos.y() < canvas.size_rect.y(): | |||
self.setPos(pos.x(), canvas.size_rect.y()) | |||
elif pos.y() + self.p_height > canvas.size_rect.height(): | |||
self.setPos(pos.x(), canvas.size_rect.height() - self.p_height) | |||
def checkItemPos(self, blockSignals): | |||
if canvas.size_rect.isNull(): | |||
return | |||
pos = self.scenePos() | |||
if (canvas.size_rect.contains(pos) and | |||
canvas.size_rect.contains(pos + QPointF(self.p_width, self.p_height))): | |||
return | |||
if blockSignals: | |||
self.blockSignals(True) | |||
if pos.x() < canvas.size_rect.x(): | |||
self.setPos(canvas.size_rect.x(), pos.y()) | |||
elif pos.x() + self.p_width > canvas.size_rect.width(): | |||
self.setPos(canvas.size_rect.width() - self.p_width, pos.y()) | |||
pos = self.scenePos() | |||
if pos.y() < canvas.size_rect.y(): | |||
self.setPos(pos.x(), canvas.size_rect.y()) | |||
elif pos.y() + self.p_height > canvas.size_rect.height(): | |||
self.setPos(pos.x(), canvas.size_rect.height() - self.p_height) | |||
if blockSignals: | |||
self.blockSignals(False) | |||
def removeIconFromScene(self): | |||
if self.icon_svg is None: | |||
@@ -389,6 +408,17 @@ class CanvasBox(QGraphicsItem): | |||
connection.widget.setZValue(z_value) | |||
def triggerSignalPositionChanged(self): | |||
self.positionChanged.emit(self.m_group_id, self.m_splitted, self.x(), self.y()) | |||
self.m_will_signal_pos_change = False | |||
@pyqtSlot() | |||
def slot_signalPositionChangedLater(self): | |||
if self.m_will_signal_pos_change: | |||
return | |||
self.m_will_signal_pos_change = True | |||
QTimer.singleShot(0, self.triggerSignalPositionChanged) | |||
def type(self): | |||
return CanvasBoxType | |||
@@ -570,14 +600,14 @@ class CanvasBox(QGraphicsItem): | |||
event.accept() | |||
canvas.callback(ACTION_PLUGIN_REMOVE, self.m_plugin_id, 0, "") | |||
return | |||
QGraphicsItem.keyPressEvent(self, event) | |||
QGraphicsObject.keyPressEvent(self, event) | |||
def hoverEnterEvent(self, event): | |||
if options.auto_select_items: | |||
if len(canvas.scene.selectedItems()) > 0: | |||
canvas.scene.clearSelection() | |||
self.setSelected(True) | |||
QGraphicsItem.hoverEnterEvent(self, event) | |||
QGraphicsObject.hoverEnterEvent(self, event) | |||
def mouseDoubleClickEvent(self, event): | |||
if self.m_plugin_id >= 0: | |||
@@ -585,7 +615,7 @@ class CanvasBox(QGraphicsItem): | |||
canvas.callback(ACTION_PLUGIN_SHOW_UI if self.m_plugin_ui else ACTION_PLUGIN_EDIT, self.m_plugin_id, 0, "") | |||
return | |||
QGraphicsItem.mouseDoubleClickEvent(self, event) | |||
QGraphicsObject.mouseDoubleClickEvent(self, event) | |||
def mousePressEvent(self, event): | |||
canvas.last_z_value += 1 | |||
@@ -612,7 +642,7 @@ class CanvasBox(QGraphicsItem): | |||
else: | |||
self.m_mouse_down = False | |||
QGraphicsItem.mousePressEvent(self, event) | |||
QGraphicsObject.mousePressEvent(self, event) | |||
def mouseMoveEvent(self, event): | |||
if self.m_mouse_down: | |||
@@ -620,7 +650,7 @@ class CanvasBox(QGraphicsItem): | |||
self.setCursor(QCursor(Qt.SizeAllCursor)) | |||
self.m_cursor_moving = True | |||
self.repaintLines() | |||
QGraphicsItem.mouseMoveEvent(self, event) | |||
QGraphicsObject.mouseMoveEvent(self, event) | |||
def mouseReleaseEvent(self, event): | |||
if self.m_cursor_moving: | |||
@@ -628,11 +658,13 @@ class CanvasBox(QGraphicsItem): | |||
QTimer.singleShot(0, self.fixPos) | |||
self.m_mouse_down = False | |||
self.m_cursor_moving = False | |||
QGraphicsItem.mouseReleaseEvent(self, event) | |||
QGraphicsObject.mouseReleaseEvent(self, event) | |||
def fixPos(self): | |||
self.blockSignals(True) | |||
self.setX(round(self.x())) | |||
self.setY(round(self.y())) | |||
self.blockSignals(False) | |||
def boundingRect(self): | |||
return QRectF(0, 0, self.p_width, self.p_height) | |||
@@ -138,6 +138,36 @@ class CanvasObject(QObject): | |||
CanvasCallback(ACTION_PORTS_DISCONNECT, connectionId, 0, "") | |||
@pyqtSlot(int, bool, int, int) | |||
def boxPositionChanged(self, groupId, split, x, y): | |||
x2 = y2 = 0 | |||
if split: | |||
for group in canvas.group_list: | |||
if group.group_id == groupId: | |||
if group.split: | |||
pos = group.widgets[1].pos() | |||
x2 = pos.x() | |||
y2 = pos.y() | |||
break | |||
valueStr = "%i:%i:%i:%i" % (x, y, x2, y2) | |||
CanvasCallback(ACTION_GROUP_POSITION, groupId, 0, valueStr) | |||
@pyqtSlot(int, bool, int, int) | |||
def sboxPositionChanged(self, groupId, split, x2, y2): | |||
x = y = 0 | |||
for group in canvas.group_list: | |||
if group.group_id == groupId: | |||
pos = group.widgets[0].pos() | |||
x = pos.x() | |||
y = pos.y() | |||
break | |||
valueStr = "%i:%i:%i:%i" % (x, y, x2, y2) | |||
CanvasCallback(ACTION_GROUP_POSITION, groupId, 0, valueStr) | |||
# ------------------------------------------------------------------------------------------------------------ | |||
def getStoredCanvasPosition(key, fallback_pos): | |||
@@ -201,11 +231,17 @@ def clear(): | |||
if canvas.debug: | |||
print("PatchCanvas::clear()") | |||
group_pos = {} | |||
group_list_ids = [] | |||
port_list_ids = [] | |||
connection_list_ids = [] | |||
for group in canvas.group_list: | |||
group_pos[group.group_name] = ( | |||
group.split, | |||
group.widgets[0].pos(), | |||
group.widgets[1].pos() if group.split else None, | |||
) | |||
group_list_ids.append(group.group_id) | |||
for port in canvas.port_list: | |||
@@ -230,6 +266,7 @@ def clear(): | |||
canvas.port_list = [] | |||
canvas.connection_list = [] | |||
canvas.group_plugin_map = {} | |||
canvas.old_group_pos = group_pos | |||
canvas.scene.clearSelection() | |||
@@ -278,6 +315,8 @@ def addGroup(group_id, group_name, split=SPLIT_UNDEF, icon=ICON_APPLICATION): | |||
group_id, group_name.encode(), split2str(split), icon2str(icon))) | |||
return | |||
old_matching_group = canvas.old_group_pos.pop(group_name, None) | |||
if split == SPLIT_UNDEF: | |||
isHardware = bool(icon == ICON_HARDWARE) | |||
@@ -285,8 +324,11 @@ def addGroup(group_id, group_name, split=SPLIT_UNDEF, icon=ICON_APPLICATION): | |||
split = getStoredCanvasSplit(group_name, SPLIT_YES if isHardware else split) | |||
elif isHardware: | |||
split = SPLIT_YES | |||
elif old_matching_group is not None and old_matching_group[0]: | |||
split = SPLIT_YES | |||
group_box = CanvasBox(group_id, group_name, icon) | |||
group_box.positionChanged.connect(canvas.qobject.boxPositionChanged) | |||
group_dict = group_dict_t() | |||
group_dict.group_id = group_id | |||
@@ -301,20 +343,29 @@ def addGroup(group_id, group_name, split=SPLIT_UNDEF, icon=ICON_APPLICATION): | |||
if split == SPLIT_YES: | |||
group_box.setSplit(True, PORT_MODE_OUTPUT) | |||
group_box.blockSignals(True) | |||
if features.handle_group_pos: | |||
group_box.setPos(getStoredCanvasPosition(group_name + "_OUTPUT", CanvasGetNewGroupPos(False))) | |||
elif old_matching_group is not None: | |||
group_box.setPos(old_matching_group[1]) | |||
else: | |||
group_box.setPos(CanvasGetNewGroupPos(False)) | |||
group_box.blockSignals(False) | |||
group_sbox = CanvasBox(group_id, group_name, icon) | |||
group_sbox.setSplit(True, PORT_MODE_INPUT) | |||
group_sbox.positionChanged.connect(canvas.qobject.sboxPositionChanged) | |||
group_dict.widgets[1] = group_sbox | |||
group_sbox.blockSignals(True) | |||
if features.handle_group_pos: | |||
group_sbox.setPos(getStoredCanvasPosition(group_name + "_INPUT", CanvasGetNewGroupPos(True))) | |||
elif old_matching_group is not None and old_matching_group[0]: | |||
group_sbox.setPos(old_matching_group[2]) | |||
else: | |||
group_sbox.setPos(CanvasGetNewGroupPos(True)) | |||
group_sbox.blockSignals(False) | |||
canvas.last_z_value += 1 | |||
group_sbox.setZValue(canvas.last_z_value) | |||
@@ -322,19 +373,21 @@ def addGroup(group_id, group_name, split=SPLIT_UNDEF, icon=ICON_APPLICATION): | |||
if options.eyecandy == EYECANDY_FULL and not options.auto_hide_groups: | |||
CanvasItemFX(group_sbox, True, False) | |||
group_sbox.checkItemPos() | |||
group_sbox.checkItemPos(True) | |||
else: | |||
group_box.setSplit(False) | |||
if features.handle_group_pos: | |||
group_box.setPos(getStoredCanvasPosition(group_name, CanvasGetNewGroupPos(False))) | |||
elif old_matching_group is not None: | |||
group_box.setPos(old_matching_group[1]) | |||
else: | |||
# Special ladish fake-split groups | |||
horizontal = bool(icon == ICON_HARDWARE or icon == ICON_LADISH_ROOM) | |||
group_box.setPos(CanvasGetNewGroupPos(horizontal)) | |||
group_box.checkItemPos() | |||
group_box.checkItemPos(True) | |||
canvas.last_z_value += 1 | |||
group_box.setZValue(canvas.last_z_value) | |||
@@ -630,10 +683,14 @@ def restoreGroupPositions(dataList): | |||
if group is None: | |||
continue | |||
group.widgets[0].blockSignals(True) | |||
group.widgets[0].setPos(data['pos1x'], data['pos1y']) | |||
group.widgets[0].blockSignals(False) | |||
if group.split and group.widgets[1]: | |||
group.widgets[1].blockSignals(True) | |||
group.widgets[1].setPos(data['pos2x'], data['pos2y']) | |||
group.widgets[1].blockSignals(False) | |||
def setGroupPos(group_id, group_pos_x, group_pos_y): | |||
setGroupPosFull(group_id, group_pos_x, group_pos_y, group_pos_x, group_pos_y) | |||
@@ -645,10 +702,14 @@ def setGroupPosFull(group_id, group_pos_x_o, group_pos_y_o, group_pos_x_i, group | |||
for group in canvas.group_list: | |||
if group.group_id == group_id: | |||
group.widgets[0].blockSignals(True) | |||
group.widgets[0].setPos(group_pos_x_o, group_pos_y_o) | |||
group.widgets[0].blockSignals(False) | |||
if group.split and group.widgets[1]: | |||
group.widgets[1].blockSignals(True) | |||
group.widgets[1].setPos(group_pos_x_i, group_pos_y_i) | |||
group.widgets[1].blockSignals(False) | |||
QTimer.singleShot(0, canvas.scene.update) | |||
return | |||
@@ -58,7 +58,6 @@ class RubberbandRect(QGraphicsRectItem): | |||
class PatchScene(QGraphicsScene): | |||
scaleChanged = pyqtSignal(float) | |||
sceneGroupMoved = pyqtSignal(int, int, QPointF) | |||
pluginSelected = pyqtSignal(list) | |||
def __init__(self, parent, view): | |||
@@ -367,8 +366,7 @@ class PatchScene(QGraphicsScene): | |||
items_list = self.selectedItems() | |||
for item in items_list: | |||
if item and item.isVisible() and item.type() == CanvasBoxType: | |||
item.checkItemPos() | |||
self.sceneGroupMoved.emit(item.getGroupId(), item.getSplittedMode(), item.scenePos()) | |||
item.checkItemPos(False) | |||
if len(items_list) > 1: | |||
canvas.scene.update() | |||
@@ -315,6 +315,8 @@ const char* EngineCallbackOpcode2Str(const EngineCallbackOpcode opcode) noexcept | |||
return "ENGINE_CALLBACK_PATCHBAY_PORT_GROUP_CHANGED"; | |||
case ENGINE_CALLBACK_PARAMETER_MAPPED_RANGE_CHANGED: | |||
return "ENGINE_CALLBACK_PARAMETER_MAPPED_RANGE_CHANGED"; | |||
case ENGINE_CALLBACK_PATCHBAY_CLIENT_POSITION_CHANGED: | |||
return "ENGINE_CALLBACK_PATCHBAY_CLIENT_POSITION_CHANGED"; | |||
} | |||
carla_stderr("CarlaBackend::EngineCallbackOpcode2Str(%i) - invalid opcode", opcode); | |||
@@ -1,6 +1,6 @@ | |||
/* | |||
* Carla patchbay utils | |||
* Copyright (C) 2011-2017 Filipe Coelho <falktx@falktx.com> | |||
* Copyright (C) 2011-2020 Filipe Coelho <falktx@falktx.com> | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU General Public License as | |||
@@ -27,7 +27,7 @@ | |||
struct GroupNameToId { | |||
uint group; | |||
char name[STR_MAX+1]; // globally unique | |||
char name[STR_MAX]; // globally unique | |||
void clear() noexcept | |||
{ | |||
@@ -43,15 +43,15 @@ struct GroupNameToId { | |||
void rename(const char n[]) noexcept | |||
{ | |||
std::strncpy(name, n, STR_MAX); | |||
name[STR_MAX] = '\0'; | |||
std::strncpy(name, n, STR_MAX-1); | |||
name[STR_MAX-1] = '\0'; | |||
} | |||
bool operator==(const GroupNameToId& groupNameToId) const noexcept | |||
{ | |||
if (groupNameToId.group != group) | |||
return false; | |||
if (std::strncmp(groupNameToId.name, name, STR_MAX) != 0) | |||
if (std::strncmp(groupNameToId.name, name, STR_MAX-1) != 0) | |||
return false; | |||
return true; | |||
} | |||
@@ -65,10 +65,12 @@ struct GroupNameToId { | |||
struct PatchbayGroupList { | |||
uint lastId; | |||
LinkedList<GroupNameToId> list; | |||
CarlaMutex mutex; | |||
PatchbayGroupList() noexcept | |||
: lastId(0), | |||
list() {} | |||
list(), | |||
mutex() {} | |||
void clear() noexcept | |||
{ | |||
@@ -87,8 +89,8 @@ struct PatchbayGroupList { | |||
struct PortNameToId { | |||
uint group; | |||
uint port; | |||
char name[STR_MAX+1]; // locally unique (within the same group) | |||
char fullName[STR_MAX+1]; // globally unique | |||
char name[STR_MAX]; // locally unique (within the same group) | |||
char fullName[STR_MAX]; // globally unique | |||
void clear() noexcept | |||
{ | |||
@@ -107,26 +109,26 @@ struct PortNameToId { | |||
void setFullName(const char fn[]) noexcept | |||
{ | |||
std::strncpy(fullName, fn, STR_MAX); | |||
fullName[STR_MAX] = '\0'; | |||
std::strncpy(fullName, fn, STR_MAX-1); | |||
fullName[STR_MAX-1] = '\0'; | |||
} | |||
void rename(const char n[], const char fn[]) noexcept | |||
{ | |||
std::strncpy(name, n, STR_MAX); | |||
name[STR_MAX] = '\0'; | |||
std::strncpy(name, n, STR_MAX-1); | |||
name[STR_MAX-1] = '\0'; | |||
std::strncpy(fullName, fn, STR_MAX); | |||
fullName[STR_MAX] = '\0'; | |||
std::strncpy(fullName, fn, STR_MAX-1); | |||
fullName[STR_MAX-1] = '\0'; | |||
} | |||
bool operator==(const PortNameToId& portNameToId) noexcept | |||
{ | |||
if (portNameToId.group != group || portNameToId.port != port) | |||
return false; | |||
if (std::strncmp(portNameToId.name, name, STR_MAX) != 0) | |||
if (std::strncmp(portNameToId.name, name, STR_MAX-1) != 0) | |||
return false; | |||
if (std::strncmp(portNameToId.fullName, fullName, STR_MAX) != 0) | |||
if (std::strncmp(portNameToId.fullName, fullName, STR_MAX-1) != 0) | |||
return false; | |||
return true; | |||
} | |||
@@ -140,10 +142,12 @@ struct PortNameToId { | |||
struct PatchbayPortList { | |||
uint lastId; | |||
LinkedList<PortNameToId> list; | |||
CarlaMutex mutex; | |||
PatchbayPortList() noexcept | |||
: lastId(0), | |||
list() {} | |||
list(), | |||
mutex() {} | |||
void clear() noexcept | |||
{ | |||