MOD Gui support.

This commit is contained in:
Robin E. R. Davies
2025-07-10 11:37:02 -04:00
parent e7cdd38056
commit 890368352f
45 changed files with 1971 additions and 806 deletions
+182 -63
View File
@@ -24,22 +24,20 @@
using namespace pipedal;
// non-localizable. RFC 7231 Date Format
;
static const char*INVARIANT_WEEK_DAYS[] =
{ "Sun","Mon", "Tue", "Wed","Thu", "Fri","Sat"};
static const char *INVARIANT_WEEK_DAYS[] =
{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
// non-localizable. RFC 7231 Date Format
static const char*INVARIANT_MONTHS[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep","Oct","Nov", "Dec"};
static const char *INVARIANT_MONTHS[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
// non-localizable. RFC 7231 Date Format
static const char*FORMAT = "%s, %02d %s %04d %02d:%02d:%02d GMT";
static const char *FORMAT = "%s, %02d %s %04d %02d:%02d:%02d GMT";
std::string HtmlHelper::timeToHttpDate()
{
return timeToHttpDate(time(nullptr));
}
std::string HtmlHelper::timeToHttpDate(std::filesystem::file_time_type time)
{
@@ -56,29 +54,30 @@ std::string HtmlHelper::timeToHttpDate(time_t time)
struct tm gmTm;
if (gmtime_r(&time,&gmTm) == nullptr)
if (gmtime_r(&time, &gmTm) == nullptr)
{
return "Mon, 01 Jan 1970 00:00:00 GMT";
}
char szBuffer[sizeof("Mon, 01 Jan 1970 00:00:00 GMT+")];
snprintf(szBuffer,sizeof(szBuffer), FORMAT,
INVARIANT_WEEK_DAYS[gmTm.tm_wday],
gmTm.tm_mday,
INVARIANT_MONTHS[gmTm.tm_mon],
gmTm.tm_year+1900,
gmTm.tm_hour,
gmTm.tm_min,
gmTm.tm_sec);
snprintf(szBuffer, sizeof(szBuffer), FORMAT,
INVARIANT_WEEK_DAYS[gmTm.tm_wday],
gmTm.tm_mday,
INVARIANT_MONTHS[gmTm.tm_mon],
gmTm.tm_year + 1900,
gmTm.tm_hour,
gmTm.tm_min,
gmTm.tm_sec);
return szBuffer;
}
static int hexN(char c)
{
if (c >= '0' && c <= '9') return c-'0';
if (c >= 'a' && c <= 'f') return 10+c-'a';
if (c >= 'A' && c <= 'F') return 10+c-'A';
if (c >= '0' && c <= '9')
return c - '0';
if (c >= 'a' && c <= 'f')
return 10 + c - 'a';
if (c >= 'A' && c <= 'F')
return 10 + c - 'A';
throw std::invalid_argument("Malformed URL.");
}
static char hexChar(char c1, char c2)
@@ -86,15 +85,19 @@ static char hexChar(char c1, char c2)
return (char)((hexN(c1) << 4) + hexN(c2));
}
class SafeCharacterMap {
class SafeCharacterMap
{
bool safeCharacter[256];
public:
SafeCharacterMap()
SafeCharacterMap()
{
for (int i = 0; i < 0x20; ++i) {
for (int i = 0; i < 0x20; ++i)
{
safeCharacter[i] = false;
}
for (int i = 0x20; i < 0x80; ++i) {
for (int i = 0x20; i < 0x80; ++i)
{
safeCharacter[i] = true;
}
for (int i = 0x80; i < 256; ++i)
@@ -102,27 +105,25 @@ public:
safeCharacter[i] = false;
}
const char *unsafeCharacters = ":/+?=#%@ ";
for (const char*p = unsafeCharacters; *p != 0; ++p)
for (const char *p = unsafeCharacters; *p != 0; ++p)
{
safeCharacter[(uint8_t)*p] = false;
}
}
bool isSafeCharacter(char c) { return safeCharacter[(uint8_t)c];}
bool isSafeCharacter(char c) { return safeCharacter[(uint8_t)c]; }
} g_safeCharacterMap;
std::string HtmlHelper::encode_url_segment(const char*pStart, const char*pEnd, bool isQuerySegment )
std::string HtmlHelper::encode_url_segment(const char *pStart, const char *pEnd, bool isQuerySegment)
{
std::stringstream s;
encode_url_segment(s,pStart,pEnd,isQuerySegment);
encode_url_segment(s, pStart, pEnd, isQuerySegment);
return s.str();
}
static char hexChars[] = "0123456789ABCDEF";
void HtmlHelper::encode_url_segment(std::ostream&os, const char*pStart, const char *pEnd, bool isQuerySegment)
void HtmlHelper::encode_url_segment(std::ostream &os, const char *pStart, const char *pEnd, bool isQuerySegment)
{
while (pStart != pEnd)
{
@@ -130,21 +131,21 @@ void HtmlHelper::encode_url_segment(std::ostream&os, const char*pStart, const ch
if (g_safeCharacterMap.isSafeCharacter(c))
{
os << c;
} else if (c == ' ' && isQuerySegment)
}
else if (c == ' ' && isQuerySegment)
{
os << '+';
os << c;
} else {
}
else
{
os << '%' << hexChars[(c >> 4) & 0x0f] << hexChars[c & 0x0F];
}
}
}
std::string HtmlHelper::decode_url_segment(const char*pStart, const char*pEnd, bool isQuery)
std::string HtmlHelper::decode_url_segment(const char *pStart, const char *pEnd, bool isQuery)
{
std::stringstream s;
auto p = pStart;
@@ -155,16 +156,23 @@ std::string HtmlHelper::decode_url_segment(const char*pStart, const char*pEnd, b
if (c == '+')
{
s << ' ';
} else if (c != '%')
}
else if (c != '%')
{
s << c;
} else {
if (pStart+2 < pEnd)
}
else
{
if (pStart + 2 < pEnd)
{
char c1 = *p;
++p;
char c2 = *p;
++p;
s << hexChar(c1, c2);
}
else
{
char c1 = *p; ++p;
char c2 = *p; ++p;
s << hexChar(c1,c2);
} else {
throw std::invalid_argument("Malformed URL.");
}
}
@@ -172,9 +180,9 @@ std::string HtmlHelper::decode_url_segment(const char*pStart, const char*pEnd, b
return s.str();
}
std::string HtmlHelper::decode_url_segment(const char*text, bool isQuery)
std::string HtmlHelper::decode_url_segment(const char *text, bool isQuery)
{
return decode_url_segment(text, text + strlen(text),isQuery);
return decode_url_segment(text, text + strlen(text), isQuery);
}
void HtmlHelper::utf32_to_utf8_stream(std::ostream &s, uint32_t uc)
@@ -182,20 +190,27 @@ void HtmlHelper::utf32_to_utf8_stream(std::ostream &s, uint32_t uc)
if (uc < 0x80u)
{
s << (char)uc;
} else if (uc < 0x800u) {
}
else if (uc < 0x800u)
{
s << (char)(0xC0 + (uc >> 6));
s << (char)(0x80 + (uc & 0x3F));
} else if (uc < 0x10000u) {
}
else if (uc < 0x10000u)
{
s << (char)(0xE0 + (uc >> 12));
s << (char)(0x80 + ((uc >> 6) & 0x3F));
s << (char)(0x80 + (uc & 0x3F));
} else if (uc < 0x0110000) {
}
else if (uc < 0x0110000)
{
s << (char)(0xF0 + (uc >> 18));
s << (char)(0x80 + ((uc >> 12) & 0x3F));
s << (char)(0x80 + ((uc >> 6) & 0x3F));
s << (char)(0x80 + (uc & 0x3F));
} else {
}
else
{
throw std::range_error("Illegal UTF-32 character.");
}
}
@@ -205,20 +220,22 @@ const std::string ESPECIALS = "()<>@,;:\"/[]?.=";
#define MAX_FILENAME_LENGTH 96
#define MAX_SEGMENT_LENGTH 74 // 75 per RFC2047.2. But leave space for a ';' to allow easier breaking of lines.
std::string HtmlHelper::Rfc5987EncodeFileName(const std::string&name)
std::string HtmlHelper::Rfc5987EncodeFileName(const std::string &name)
{
// Encode all characters. Let the browser handle safe-ifying the actual name, since that's os dependent.
std::stringstream s;
std::string prefix = "UTF-8''";
std::string postfix = "";
s << prefix;
for (char c: name)
for (char c : name)
{
uint8_t uc = (uint8_t)c;
if (uc < 0x20 || uc >= 0x80 || ESPECIALS.find(c) != std::string::npos)
{
s << '%' << hexChars[uc >> 4] << hexChars[uc & 0x0F];
} else {
}
else
{
s << c;
}
}
@@ -226,7 +243,6 @@ std::string HtmlHelper::Rfc5987EncodeFileName(const std::string&name)
return s.str();
}
#define MAX_FILE_NAME_LENGHT 96
const std::string SF_SPECIALS = " <>@;:\"\'/[]?=";
@@ -249,23 +265,126 @@ std::string HtmlHelper::SafeFileName(const std::string &name)
return s.str();
}
std::string HtmlHelper::HtmlEncode(const std::string& text)
std::string HtmlHelper::HtmlEncode(const std::string &text)
{
std::stringstream os;
for (char c: text)
for (char c : text)
{
switch(c)
switch (c)
{
case '<': os << "&lt;"; break;
case '>': os << "&gt;"; break;
case '&': os << "&amp;"; break;
default: os << c; break;
case '<':
os << "&lt;";
break;
case '>':
os << "&gt;";
break;
case '&':
os << "&amp;";
break;
default:
os << c;
break;
}
}
return os.str();
}
static const uint64_t crc64_table[256] = {
0x0000000000000000ULL, 0x42F0E1EBA9EA3693ULL, 0x85E1C3D753D46D26ULL, 0xC711223CFA3E5BB5ULL,
0x493366450E42ECDFULL, 0x0BC387AEA7A8DA4CULL, 0xCCD2A5925D9681F9ULL, 0x8E224479F47CB76AULL,
0x9266CC8A1C85D9BEULL, 0xD0962D61B56FEF2DULL, 0x17870F5D4F51B498ULL, 0x5577EEB6E6BB820BULL,
0xDB55AACF12C73561ULL, 0x99A54B24BB2D03F2ULL, 0x5EB4691841135847ULL, 0x1C4488F3E8F96ED4ULL,
0x663D78FF90E185EFULL, 0x24CD9914390BB37CULL, 0xE3DCBB28C335E8C9ULL, 0xA12C5AC36ADFDE5AULL,
0x2F0E1EBA9EA36930ULL, 0x6DFEFF5137495FA3ULL, 0xAAEFDD6DCD770416ULL, 0xE81F3C86649D3285ULL,
0xF45BB4758C645C51ULL, 0xB6AB559E258E6AC2ULL, 0x71BA77A2DFB03177ULL, 0x334A9649765A07E4ULL,
0xBD68D2308226B08EULL, 0xFF9833DB2BCC861DULL, 0x388911E7D1F2DDA8ULL, 0x7A79F00C7818EB3BULL,
0xCC7AF1FF21C30BDEULL, 0x8E8A101488293D4DULL, 0x499B3228721766F8ULL, 0x0B6BD3C3DBFD506BULL,
0x854997BA2F81E701ULL, 0xC7B97651866BD192ULL, 0x00A8546D7C558A27ULL, 0x4258B586D5BFBCB4ULL,
0x5E1C3D753D46D260ULL, 0x1CECDC9E94ACE4F3ULL, 0xDBFDFEA26E92BF46ULL, 0x990D1F49C77889D5ULL,
0x172F5B3033043EBFULL, 0x55DFBADB9AEE082CULL, 0x92CE98E760D05399ULL, 0xD03E790CC93A650AULL,
0xAA478900B1228E31ULL, 0xE8B768EB18C8B8A2ULL, 0x2FA64AD7E2F6E317ULL, 0x6D56AB3C4B1CD584ULL,
0xE374EF45BF6062EEULL, 0xA1840EAE168A547DULL, 0x66952C92ECB40FC8ULL, 0x2465CD79455E395BULL,
0x3821458AADA7578FULL, 0x7AD1A461044D611CULL, 0xBDC0865DFE733AA9ULL, 0xFF3067B657990C3AULL,
0x711223CFA3E5BB50ULL, 0x33E2C2240A0F8DC3ULL, 0xF4F3E018F031D676ULL, 0xB60301F359DBE0E5ULL,
0xDA050215EA6C212FULL, 0x98F5E3FE438617BCULL, 0x5FE4C1C2B9B84C09ULL, 0x1D14202910527A9AULL,
0x93366450E42ECDF0ULL, 0xD1C685BB4DC4FB63ULL, 0x16D7A787B7FAA0D6ULL, 0x5427466C1E109645ULL,
0x4863CE9FF6E9F891ULL, 0x0A932F745F03CE02ULL, 0xCD820D48A53D95B7ULL, 0x8F72ECA30CD7A324ULL,
0x0150A8DAF8AB144EULL, 0x43A04931514122DDULL, 0x84B16B0DAB7F7968ULL, 0xC6418AE602954FFBULL,
0xBC387AEA7A8DA4C0ULL, 0xFEC89B01D3679253ULL, 0x39D9B93D2959C9E6ULL, 0x7B2958D680B3FF75ULL,
0xF50B1CAF74CF481FULL, 0xB7FBFD44DD257E8CULL, 0x70EADF78271B2539ULL, 0x321A3E938EF113AAULL,
0x2E5EB66066087D7EULL, 0x6CAE578BCFE24BEDULL, 0xABBF75B735DC1058ULL, 0xE94F945C9C3626CBULL,
0x676DD025684A91A1ULL, 0x259D31CEC1A0A732ULL, 0xE28C13F23B9EFC87ULL, 0xA07CF2199274CA14ULL,
0x167FF3EACBAF2AF1ULL, 0x548F120162451C62ULL, 0x939E303D987B47D7ULL, 0xD16ED1D631917144ULL,
0x5F4C95AFC5EDC62EULL, 0x1DBC74446C07F0BDULL, 0xDAAD56789639AB08ULL, 0x985DB7933FD39D9BULL,
0x84193F60D72AF34FULL, 0xC6E9DE8B7EC0C5DCULL, 0x01F8FCB784FE9E69ULL, 0x43081D5C2D14A8FAULL,
0xCD2A5925D9681F90ULL, 0x8FDAB8CE70822903ULL, 0x48CB9AF28ABC72B6ULL, 0x0A3B7B1923564425ULL,
0x70428B155B4EAF1EULL, 0x32B26AFEF2A4998DULL, 0xF5A348C2089AC238ULL, 0xB753A929A170F4ABULL,
0x3971ED50550C43C1ULL, 0x7B810CBBFCE67552ULL, 0xBC902E8706D82EE7ULL, 0xFE60CF6CAF321874ULL,
0xE224479F47CB76A0ULL, 0xA0D4A674EE214033ULL, 0x67C58448141F1B86ULL, 0x253565A3BDF52D15ULL,
0xAB1721DA49899A7FULL, 0xE9E7C031E063ACECULL, 0x2EF6E20D1A5DF759ULL, 0x6C0603E6B3B7C1CAULL,
0xF6FAE5C07D3274CDULL, 0xB40A042BD4D8425EULL, 0x731B26172EE619EBULL, 0x31EBC7FC870C2F78ULL,
0xBFC9838573709812ULL, 0xFD39626EDA9AAE81ULL, 0x3A28405220A4F534ULL, 0x78D8A1B9894EC3A7ULL,
0x649C294A61B7AD73ULL, 0x266CC8A1C85D9BE0ULL, 0xE17DEA9D3263C055ULL, 0xA38D0B769B89F6C6ULL,
0x2DAF4F0F6FF541ACULL, 0x6F5FAEE4C61F773FULL, 0xA84E8CD83C212C8AULL, 0xEABE6D3395CB1A19ULL,
0x90C79D3FEDD3F122ULL, 0xD2377CD44439C7B1ULL, 0x15265EE8BE079C04ULL, 0x57D6BF0317EDAA97ULL,
0xD9F4FB7AE3911DFDULL, 0x9B041A914A7B2B6EULL, 0x5C1538ADB04570DBULL, 0x1EE5D94619AF4648ULL,
0x02A151B5F156289CULL, 0x4051B05E58BC1E0FULL, 0x87409262A28245BAULL, 0xC5B073890B687329ULL,
0x4B9237F0FF14C443ULL, 0x0962D61B56FEF2D0ULL, 0xCE73F427ACC0A965ULL, 0x8C8315CC052A9FF6ULL,
0x3A80143F5CF17F13ULL, 0x7870F5D4F51B4980ULL, 0xBF61D7E80F251235ULL, 0xFD913603A6CF24A6ULL,
0x73B3727A52B393CCULL, 0x31439391FB59A55FULL, 0xF652B1AD0167FEEAULL, 0xB4A25046A88DC879ULL,
0xA8E6D8B54074A6ADULL, 0xEA16395EE99E903EULL, 0x2D071B6213A0CB8BULL, 0x6FF7FA89BA4AFD18ULL,
0xE1D5BEF04E364A72ULL, 0xA3255F1BE7DC7CE1ULL, 0x64347D271DE22754ULL, 0x26C49CCCB40811C7ULL,
0x5CBD6CC0CC10FAFCULL, 0x1E4D8D2B65FACC6FULL, 0xD95CAF179FC497DAULL, 0x9BAC4EFC362EA149ULL,
0x158E0A85C2521623ULL, 0x577EEB6E6BB820B0ULL, 0x906FC95291867B05ULL, 0xD29F28B9386C4D96ULL,
0xCEDBA04AD0952342ULL, 0x8C2B41A1797F15D1ULL, 0x4B3A639D83414E64ULL, 0x09CA82762AAB78F7ULL,
0x87E8C60FDED7CF9DULL, 0xC51827E4773DF90EULL, 0x020905D88D03A2BBULL, 0x40F9E43324E99428ULL,
0x2CFFE7D5975E55E2ULL, 0x6E0F063E3EB46371ULL, 0xA91E2402C48A38C4ULL, 0xEBEEC5E96D600E57ULL,
0x65CC8190991CB93DULL, 0x273C607B30F68FAEULL, 0xE02D4247CAC8D41BULL, 0xA2DDA3AC6322E288ULL,
0xBE992B5F8BDB8C5CULL, 0xFC69CAB42231BACFULL, 0x3B78E888D80FE17AULL, 0x7988096371E5D7E9ULL,
0xF7AA4D1A85996083ULL, 0xB55AACF12C735610ULL, 0x724B8ECDD64D0DA5ULL, 0x30BB6F267FA73B36ULL,
0x4AC29F2A07BFD00DULL, 0x08327EC1AE55E69EULL, 0xCF235CFD546BBD2BULL, 0x8DD3BD16FD818BB8ULL,
0x03F1F96F09FD3CD2ULL, 0x41011884A0170A41ULL, 0x86103AB85A2951F4ULL, 0xC4E0DB53F3C36767ULL,
0xD8A453A01B3A09B3ULL, 0x9A54B24BB2D03F20ULL, 0x5D45907748EE6495ULL, 0x1FB5719CE1045206ULL,
0x919735E51578E56CULL, 0xD367D40EBC92D3FFULL, 0x1476F63246AC884AULL, 0x568617D9EF46BED9ULL,
0xE085162AB69D5E3CULL, 0xA275F7C11F7768AFULL, 0x6564D5FDE549331AULL, 0x279434164CA30589ULL,
0xA9B6706FB8DFB2E3ULL, 0xEB46918411358470ULL, 0x2C57B3B8EB0BDFC5ULL, 0x6EA7525342E1E956ULL,
0x72E3DAA0AA188782ULL, 0x30133B4B03F2B111ULL, 0xF7021977F9CCEAA4ULL, 0xB5F2F89C5026DC37ULL,
0x3BD0BCE5A45A6B5DULL, 0x79205D0E0DB05DCEULL, 0xBE317F32F78E067BULL, 0xFCC19ED95E6430E8ULL,
0x86B86ED5267CDBD3ULL, 0xC4488F3E8F96ED40ULL, 0x0359AD0275A8B6F5ULL, 0x41A94CE9DC428066ULL,
0xCF8B0890283E370CULL, 0x8D7BE97B81D4019FULL, 0x4A6ACB477BEA5A2AULL, 0x089A2AACD2006CB9ULL,
0x14DEA25F3AF9026DULL, 0x562E43B4931334FEULL, 0x913F6188692D6F4BULL, 0xD3CF8063C0C759D8ULL,
0x5DEDC41A34BBEEB2ULL, 0x1F1D25F19D51D821ULL, 0xD80C07CD676F8394ULL, 0x9AFCE626CE85B507ULL};
uint64_t HtmlHelper::crc64(uint8_t *data, size_t length, uint64_t crc)
{
crc = crc ^ uint64_t(-1);
for (size_t i = 0; i < length; ++i)
{
uint8_t c = data[i];
crc = crc64_table[(crc ^ (uint8_t)c) & 0xFF] ^ (crc >> 8);
}
return crc ^ uint64_t(-1);
}
uint64_t HtmlHelper::crc64(const std::string&value, uint64_t crc)
{
crc = crc ^ uint64_t(-1);
for (auto c: value)
{
crc = crc64_table[(crc ^ (uint8_t)c) & 0xFF] ^ (crc >> 8);
}
return crc ^ uint64_t(-1);
}
std::string HtmlHelper::generateEtag(const std::filesystem::path &path)
{
uint64_t crc = 0;
crc = crc64(path.string());
auto fTime = std::filesystem::last_write_time(path);
crc = crc64((uint8_t*)&fTime, sizeof(fTime));
return std::to_string(crc);
}
+6
View File
@@ -29,6 +29,7 @@ namespace pipedal {
class HtmlHelper {
public:
static std::string timeToHttpDate();
static std::string timeToHttpDate(time_t time);
static std::string timeToHttpDate(std::filesystem::file_time_type time);
@@ -57,6 +58,11 @@ public:
static std::string SafeFileName(const std::string &name);
static std::string HtmlEncode(const std::string& text);
static uint64_t crc64(uint8_t *data, size_t length,uint64_t crc = 0);
static uint64_t crc64(const std::string&value, uint64_t crc = 0);
static std::string generateEtag(const std::filesystem::path &path);
};
} // namespace.
+4
View File
@@ -218,6 +218,10 @@ bool pipedal::IsSubdirectory(const std::filesystem::path &path, const std::files
if ((*i) != (*iPath))
{
if (*i == "" && ++i == basePath.end()) // match xyz/ (trailing /)
{
break;
}
return false;
}
++iPath;
+41 -16
View File
@@ -100,7 +100,7 @@ Lv2Effect::Lv2Effect(
lilv_node_free(uriNode);
{
AutoLilvNode bundleUri = lilv_plugin_get_bundle_uri(pPlugin);
char *bundleUriString = lilv_file_uri_parse(bundleUri.AsUri().c_str(), nullptr);
char* bundleUriString = lilv_file_uri_parse(bundleUri.AsUri().c_str(), nullptr);
std::string storagePath = pHost_->GetPluginStoragePath();
@@ -108,7 +108,7 @@ Lv2Effect::Lv2Effect(
pHost_->GetMapFeature().GetMap(),
logFeature.GetLog(),
bundleUriString,
storagePath);
storagePath);
mapPathFeature.Prepare(&(pHost_->GetMapFeature()));
mapPathFeature.SetPluginStoragePath(pHost_->GetPluginStoragePath());
@@ -117,11 +117,12 @@ Lv2Effect::Lv2Effect(
const auto &fileProperties = info_->piPedalUI()->fileProperties();
for (const auto &fileProperty : fileProperties)
{
if (!fileProperty->resourceDirectory().empty())
{
mapPathFeature.AddResourceFileMapping({makeAbsolutePath(fileProperty->resourceDirectory(), bundleUriString),
makeAbsolutePath(fileProperty->directory(), pHost_->GetPluginStoragePath())});
}
fs::path targetPath = fileProperty->directory() / std::filesystem::path(bundleUriString).parent_path().filename();
mapPathFeature.AddResourceFileMapping({
bundleUriString,
storagePath / targetPath,
fileProperty->fileTypes()
});
}
}
@@ -243,14 +244,41 @@ Lv2Effect::Lv2Effect(
// now that we've loaded the preset, clear the uri, and save new state
// Why? because lilv doesn't provide facilities for reading state.
pedalboardItem.lv2State(this->stateInterface->Save());
RestoreState(pedalboardItem); // reload it with OUR map/unmap handling.
pedalboardItem.lilvPresetUri("");
}
else
{
RestoreState(pedalboardItem);
if (!RestoreState(pedalboardItem))
{
if (info->hasDefaultState())
{
// restore the default state.
try
{
// REsTORE from LV2_STATE__state default state.
AutoLilvNode pluginNode = lilv_new_uri(pWorld, info->uri().c_str());
LilvState *pState = lilv_state_new_from_world(pWorld, pHost->GetMapFeature().GetMap(), pluginNode);
if (pState)
{
if (this->stateInterface)
{
this->stateInterface->RestoreState(pState);
}
lilv_state_free(pState);
}
pedalboardItem.lv2State(this->stateInterface->Save());
RestoreState(pedalboardItem); // do it with OUR map/unmap file handling.
}
catch (const std::exception &e)
{
Lv2Log::warning(SS("Failed to restore default state for " << info->name() << ": " << e.what()));
}
}
}
}
}
void Lv2Effect::RestoreState(PedalboardItem &pedalboardItem)
bool Lv2Effect::RestoreState(PedalboardItem &pedalboardItem)
{
// Restore state if present.
if (this->stateInterface)
@@ -260,20 +288,18 @@ void Lv2Effect::RestoreState(PedalboardItem &pedalboardItem)
if (pedalboardItem.lv2State().isValid_)
{
this->stateInterface->Restore(pedalboardItem.lv2State());
return true;
}
else
{
// set the state to default state.
auto savedState = this->stateInterface->Save();
pedalboardItem.lv2State(savedState);
}
return false;
}
catch (const std::exception &e)
{
std::string name = pedalboardItem.pluginName();
Lv2Log::warning(SS(name << ": " << e.what()));
return false;
}
}
return true;
}
void Lv2Effect::ConnectControlPorts()
@@ -1116,4 +1142,3 @@ void Lv2Effect::SetPathPatchProperty(const std::string &propertyUri, const std::
{
mainThreadPathProperties[propertyUri] = jsonAtom;
}
+1 -1
View File
@@ -61,7 +61,7 @@ namespace pipedal
FileBrowserFilesFeature fileBrowserFilesFeature;
std::unique_ptr<StateInterface> stateInterface;
void RestoreState(PedalboardItem&pedalboardItem);
bool RestoreState(PedalboardItem&pedalboardItem);
LogFeature logFeature;
std::map<std::string,AtomBuffer> patchPropertyPrototypes;
+3
View File
@@ -65,5 +65,8 @@ namespace pipedal {
const LV2_URID_Map *GetMap() const { return &map;}
LV2_URID_Map *GetMap() { return &map;}
const LV2_URID_Unmap *GetUnmap() const { return &unmap;}
LV2_URID_Unmap *GetUnmap() { return &unmap;}
};
}
+44
View File
@@ -22,9 +22,12 @@
#include <algorithm>
#include "json.hpp"
#include <sstream>
#include "PluginHost.hpp"
#include "util.hpp"
using namespace pipedal;
namespace fs = std::filesystem;
MapPathFeature::MapPathFeature()
{
@@ -69,6 +72,27 @@ void MapPathFeature::FnFreePath(LV2_State_Free_Path_Handle handle, char* path)
return ((MapPathFeature *)handle)->AbsolutePath(abstract_path);
}
static std::string CreateMappathLink(const fs::path&sourceFile, const fs::path &resourceDirectory, const fs::path &targetDirectory)
{
try {
fs::path targetFile = targetDirectory / sourceFile.filename();
if (fs::exists(targetFile)) {
return targetFile;
}
fs::create_directories(targetDirectory);
fs::create_symlink(sourceFile,targetFile);
return targetFile;
}catch (const std::exception &e) {
Lv2Log::error(SS("Failed to convert resource file to media file. "
<< sourceFile
<< " -> "
<< targetDirectory
<< " (" << e.what() << ")"
));
return sourceFile;
}
}
char *MapPathFeature::AbsolutePath(const char *abstract_path)
{
if (strlen(abstract_path) == 0)
@@ -76,7 +100,27 @@ char *MapPathFeature::AbsolutePath(const char *abstract_path)
return strdup("");
}
std::filesystem::path t (abstract_path);
std::string extension = t.extension();
if (t.is_absolute()) {
// Handle bundle files, mapping them to the storage path for this plugin.
for (const auto&mapping: this->resourceFileMappings)
{
if (IsSubdirectory(t,mapping.resourcePath))
{
bool allowed = false;
for (const auto&fileType: mapping.fileTypes) {
if (fileType.IsValidExtension(extension)) {
allowed = true;
break;
}
}
if (allowed) {
std::string result = CreateMappathLink(t,mapping.resourcePath, mapping.storagePath);
return strdup(result.c_str());
}
}
}
return strdup(abstract_path);
}
std::filesystem::path result = storagePath / t;
+11 -2
View File
@@ -21,17 +21,24 @@
#include "lv2/state/state.h"
#include <filesystem>
#include "MapFeature.hpp"
#include <memory>
#include <vector>
#include <filesystem>
#include "PiPedalUI.hpp"
namespace pipedal
{
class Lv2PluginInfo;
struct ResourceFileMapping {
std::string resourcePath; // absolute path of the resource directory.
std::string storagePath; // absolute path of where resource directory files get placed.
std::filesystem::path resourcePath; // absolute path of the resource directory.
std::filesystem::path storagePath; // absolute path of where resource directory files get placed.
std::vector<UiFileType> fileTypes;
};
class MapPathFeature
{
public:
MapPathFeature();
void Prepare(MapFeature* map);
void SetPluginStoragePath(const std::string&path) { storagePath = path;}
@@ -46,6 +53,8 @@ namespace pipedal
const LV2_Feature*GetMakePathFeature() { return &makePathFeature;}
const LV2_Feature*GetFreePathFeature() { return &freePathFeature;}
private:
std::shared_ptr<Lv2PluginInfo> pluginInfo;
char *AbsolutePath(const char *abstract_path);
static char *FnAbsolutePath(LV2_State_Map_Path_Handle handle,
const char *abstract_path);
+1
View File
@@ -57,6 +57,7 @@ namespace pipedal
const std::vector<std::string> fileTypesX; // mixture of .ext and mime types. Use fileExtensions instead.
const std::set<std::string> fileExtensions;
};
static const std::vector<ModDirectory> &ModDirectories();
+1 -1
View File
@@ -439,7 +439,7 @@ json_variant pipedal::MakeModGuiTemplateData(
// Feed them no files; they will be added later.
json_variant filesObj = json_variant::make_array();
json_variant fileObj = json_variant::make_object();
fileObj["fileType"] = "{{filetype}}"; // magic tags that we will use to generate files at runtime.
fileObj["filetype"] = "{{filetype}}"; // magic tags that we will use to generate files at runtime.
fileObj["fullname"] = "{{fullname}}";
fileObj["basename"] = "{{basename}}";
filesObj.as_array()->push_back(fileObj);
+13
View File
@@ -115,6 +115,18 @@ const ControlValue* PedalboardItem::GetControlValue(const std::string&symbol) co
return nullptr;
}
bool Pedalboard::SetItemUseModUi(int64_t pedalItemId, bool enabled)
{
PedalboardItem*item = GetItem(pedalItemId);
if (!item) return false;
if (item->useModUi() != enabled)
{
item->useModUi(enabled);
return true;
}
return false;
}
bool Pedalboard::SetItemEnabled(int64_t pedalItemId, bool enabled)
{
PedalboardItem*item = GetItem(pedalItemId);
@@ -513,6 +525,7 @@ JSON_MAP_BEGIN(PedalboardItem)
JSON_MAP_REFERENCE(PedalboardItem,lilvPresetUri)
JSON_MAP_REFERENCE(PedalboardItem,pathProperties)
JSON_MAP_REFERENCE(PedalboardItem,title)
JSON_MAP_REFERENCE(PedalboardItem,useModUi)
JSON_MAP_END()
+3
View File
@@ -99,6 +99,7 @@ public:
std::string lilvPresetUri_;
std::map<std::string,std::string> pathProperties_;
std::string title_;
bool useModUi_ = false;
// non persistent state.
PropertyMap patchProperties;
@@ -128,6 +129,7 @@ public:
GETTER_SETTER(stateUpdateCount)
GETTER_SETTER_REF(lv2State)
GETTER_SETTER_REF(title)
GETTER_SETTER(useModUi)
Lv2PluginState&lv2State() { return lv2State_; } // non-const version.
GETTER_SETTER_REF(lilvPresetUri)
@@ -203,6 +205,7 @@ public:
bool SetControlValue(int64_t pedalItemId, const std::string &symbol, float value);
bool SetItemTitle(int64_t pedalItemId, const std::string &title);
bool SetItemEnabled(int64_t pedalItemId, bool enabled);
bool SetItemUseModUi(int64_t pedalItemId, bool enabled);
void SetCurrentSnapshotModified(bool modified);
bool IsStructureIdentical(const Pedalboard &other) const; // caan we just send a snapshot-style uddate instead of reloading plugins? All settings are ignored.
+18
View File
@@ -617,6 +617,24 @@ void PiPedalModel::UpdateCurrentPedalboard(int64_t clientId, Pedalboard &pedalbo
}
}
void PiPedalModel::SetPedalboardItemUseModUi(int64_t clientId, int64_t instanceId, bool enabled)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
{
this->pedalboard.SetItemUseModUi(instanceId, enabled);
// Notify clients.
std::vector<IPiPedalModelSubscriber::ptr> t{subscribers.begin(), subscribers.end()};
for (auto &subscriber : t)
{
subscriber->OnItemUseModUiChanged(clientId, instanceId, enabled);
}
this->SetPresetChanged(clientId, true);
}
}
void PiPedalModel::SetPedalboardItemEnable(int64_t clientId, int64_t pedalItemId, bool enabled)
{
std::lock_guard<std::recursive_mutex> guard{mutex};
+2
View File
@@ -59,6 +59,7 @@ namespace pipedal
virtual int64_t GetClientId() = 0;
virtual void OnItemEnabledChanged(int64_t clientId, int64_t pedalItemId, bool enabled) = 0;
virtual void OnItemUseModUiChanged(int64_t clientId, int64_t pedalItemId, bool enabled) = 0;
virtual void OnControlChanged(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value) = 0;
virtual void OnInputVolumeChanged(float value) = 0;
virtual void OnOutputVolumeChanged(float value) = 0;
@@ -348,6 +349,7 @@ namespace pipedal
void RemoveNotificationSubsription(std::shared_ptr<IPiPedalModelSubscriber> pSubscriber);
void SetPedalboardItemEnable(int64_t clientId, int64_t instanceId, bool enabled);
void SetPedalboardItemUseModUi(int64_t clientId, int64_t instanceId, bool enabled);
void SetControl(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value);
void PreviewControl(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value);
+30
View File
@@ -428,6 +428,22 @@ JSON_MAP_REFERENCE(PedalboardItemEnabledBody, instanceId)
JSON_MAP_REFERENCE(PedalboardItemEnabledBody, enabled)
JSON_MAP_END()
class PedalboardItemUseModGuiBody
{
public:
int64_t clientId_ = -1;
int64_t instanceId_ = -1;
bool useModUi_ = true;
DECLARE_JSON_MAP(PedalboardItemUseModGuiBody);
};
JSON_MAP_BEGIN(PedalboardItemUseModGuiBody)
JSON_MAP_REFERENCE(PedalboardItemUseModGuiBody, clientId)
JSON_MAP_REFERENCE(PedalboardItemUseModGuiBody, instanceId)
JSON_MAP_REFERENCE(PedalboardItemUseModGuiBody, useModUi)
JSON_MAP_END()
class UpdateCurrentPedalboardBody
{
public:
@@ -1303,6 +1319,11 @@ public:
pReader->read(&body);
model.SetPedalboardItemEnable(body.clientId_, body.instanceId_, body.enabled_);
}
else if (message == "setPedalboardItemUseModUi") {
PedalboardItemUseModGuiBody body;
pReader->read(&body);
model.SetPedalboardItemUseModUi(body.clientId_, body.instanceId_, body.useModUi_);
}
else if (message == "updateCurrentPedalboard")
{
{
@@ -2221,6 +2242,15 @@ private:
body.enabled_ = enabled;
Send("onItemEnabledChanged", body);
}
virtual void OnItemUseModUiChanged(int64_t clientId, int64_t pedalItemId, bool enabled)
{
PedalboardItemEnabledBody body;
body.clientId_ = clientId;
body.instanceId_ = pedalItemId;
body.enabled_ = enabled;
Send("onUseItemModUiChanged", body);
}
};
std::atomic<uint64_t> PiPedalSocketHandler::nextClientId = 0;
+8
View File
@@ -140,6 +140,7 @@ UiFileType::UiFileType(PluginHost *pHost, const LilvNode *node)
UiFileProperty::UiFileProperty(PluginHost *pHost, const LilvNode *node, const std::filesystem::path &resourcePath)
{
auto pWorld = pHost->getWorld();
@@ -455,6 +456,13 @@ bool UiFileType::IsValidExtension(const std::string&extension) const {
return false;
}
UiFileType::UiFileType(const std::string&label, const std::string &mimeType,const std::string &fileType)
: label_(label)
, mimeType_(mimeType)
, fileExtension_(fileType)
{
}
UiFileType::UiFileType(const std::string&label, const std::string &fileType)
: label_(label)
, fileExtension_(fileType)
+7
View File
@@ -75,6 +75,7 @@ namespace pipedal
{
class PluginHost;
class ModFileTypes;
class UiFileType
{
@@ -87,12 +88,16 @@ namespace pipedal
UiFileType() {}
UiFileType(PluginHost *pHost, const LilvNode *node);
UiFileType(const std::string &label, const std::string &fileType);
UiFileType(const std::string &label, const std::string&mimeType, const std::string &fileType);
static std::vector<UiFileType> GetArray(PluginHost *pHost, const LilvNode *node, const LilvNode *uri);
const std::string &label() const { return label_; }
void label(const std::string &value) { label_ = value; }
const std::string &fileExtension() const { return fileExtension_; }
void fileExtension(const std::string &value) { fileExtension_ = value; }
const std::string &mimeType() const { return mimeType_; }
void mimeType(const std::string &value) { mimeType_ = value; }
bool IsValidExtension(const std::string &extension) const;
public:
@@ -160,6 +165,8 @@ namespace pipedal
const std::vector<UiFileType> &fileTypes() const { return fileTypes_; }
std::vector<UiFileType> &fileTypes() { return fileTypes_; }
void fileTypes(const std::vector<UiFileType> &value) { fileTypes_ = value; }
void fileTypes(std::vector<UiFileType> &&value) { fileTypes_ = std::move(value); }
const std::string &patchProperty() const { return patchProperty_; }
+83 -11
View File
@@ -647,6 +647,37 @@ bool Lv2PluginInfo::HasFactoryPresets(PluginHost *lv2Host, const LilvPlugin *plu
return result;
}
static std::vector<UiFileType> ToPiPedalFileTypes(
const std::set<std::string> &modFileTypes)
{
std::vector<UiFileType> result;
for (const auto &fileType : modFileTypes)
{
UiFileType uiFileType;
std::string type = fileType;
if (type.find("/") != std::string::npos) // mime type?
{
UiFileType t = UiFileType(type,type,"");
result.push_back(t);
}
else
{
// add a leading dot.
if (!type.starts_with(".")) {
type = "." + type;
}
auto t = UiFileType(SS(type << " file"), type);
result.push_back(t);
}
}
std::sort(result.begin(), result.end(),
[](const UiFileType &left, const UiFileType &right)
{
return left.label() < right.label();
});
return result;
}
Lv2PluginInfo::FindWritablePathPropertiesResult
Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin *pPlugin)
{
@@ -710,6 +741,13 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
// "nam,nammodel"
fileTypes = mod__fileTypes.AsString();
}
std::string modFileExtensions;
AutoLilvNode mod__extensionsNode = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->mod__supportedExtensions, nullptr);
{
// "wav,flac,mp3"
modFileExtensions = mod__extensionsNode.AsString();
}
std::string fileExtensions = modFileExtensions;
ModFileTypes modFileTypes(fileTypes);
// Legacy case: audio_uploads/<plugin_directory> exists.
@@ -734,6 +772,12 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
std::string modName = modFileTypes.rootDirectories()[0];
auto modDirectory = modFileTypes.GetModDirectory(modName);
fileProperty->directory(modDirectory->pipedalPath);
std::set<std::string> fileExtensions = modDirectory->fileExtensions;
if (!modFileExtensions.empty()) {
auto extensions = split(modFileExtensions, ',');
fileExtensions = std::set<std::string>(extensions.begin(), extensions.end());
}
fileProperty->fileTypes(ToPiPedalFileTypes(fileExtensions));
}
}
@@ -750,8 +794,11 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
if (fileProperties.size() != 0)
{
std::sort(fileProperties.begin(), fileProperties.end(), [](const UiFileProperty::ptr &left, const UiFileProperty::ptr &right)
{
std::sort(
fileProperties.begin(),
fileProperties.end(),
[](const UiFileProperty::ptr &left, const UiFileProperty::ptr &right)
{
// properies with indexes first.
int32_t indexL = left->index();
if (indexL < 0)
@@ -888,7 +935,7 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP
// a lv2:Parameter?
if (lilv_world_ask(pWorld, propertyUri, lv2Host->lilvUris->isA, lv2Host->lilvUris->lv2core__Parameter))
{
Lv2PatchPropertyInfo propertyInfo{lv2Host, propertyUri};
Lv2PatchPropertyInfo propertyInfo{lv2Host, propertyUri};
propertyInfo.writable(true);
patchProperties_.push_back(std::move(propertyInfo));
}
@@ -915,18 +962,33 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP
break;
}
}
if (!found)
if (!found)
{
if (lilv_world_ask(pWorld, propertyUri, lv2Host->lilvUris->isA, lv2Host->lilvUris->lv2core__Parameter))
{
Lv2PatchPropertyInfo propertyInfo{lv2Host, propertyUri};
Lv2PatchPropertyInfo propertyInfo{lv2Host, propertyUri};
propertyInfo.readable(true);
patchProperties_.push_back(std::move(propertyInfo));
}
}
}
}
// default state.
{
AutoLilvNodes stateNodes = lilv_plugin_get_value(pPlugin, lv2Host->lilvUris->state__state);
if (stateNodes)
{
auto stateNode = lilv_nodes_get_first(stateNodes);
LilvState *defaultState = lilv_state_new_from_world(pWorld, lv2Host->GetMapFeature().GetMap(), stateNode);
if (defaultState)
{
Lv2Log::debug("Plugin %s (%s) has default state.", this->name_.c_str(), this->uri_.c_str());
this->hasDefaultState_ = true;
lilv_state_free(defaultState);
}
}
}
}
// Fetch pipedal plugin UI
@@ -976,6 +1038,10 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP
++nOutputs;
}
}
if (nOutputs == 0 || nInputs == 0)
{
isValid = false;
}
this->modGui_ = ModGui::Create(lv2Host, pPlugin);
this->is_valid_ = isValid;
}
@@ -1776,7 +1842,7 @@ Lv2PatchPropertyInfo::Lv2PatchPropertyInfo(PluginHost *pluginHost, const LilvNod
{
LilvWorld *pWorld = pluginHost->getWorld();
this->uri_ = nodeAsString(propertyUri);
AutoLilvNode range = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->rdfs__range, nullptr);
if (range)
{
@@ -1791,7 +1857,9 @@ Lv2PatchPropertyInfo::Lv2PatchPropertyInfo(PluginHost *pluginHost, const LilvNod
if (shortName)
{
this->shortName_ = shortName.AsString();
} else {
}
else
{
this->shortName_ = this->label_;
}
@@ -1799,14 +1867,16 @@ Lv2PatchPropertyInfo::Lv2PatchPropertyInfo(PluginHost *pluginHost, const LilvNod
this->index_ = indexNode.AsInt(-1);
AutoLilvNode modFileTypesNode = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->mod__fileTypes, nullptr);
if (modFileTypesNode) {
if (modFileTypesNode)
{
std::string strFileTypes = modFileTypesNode.AsString();
this->fileTypes_ = split(strFileTypes,',');
this->fileTypes_ = split(strFileTypes, ',');
}
AutoLilvNode modSupportedExtensionsNode = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->mod__supportedExtensions, nullptr);
if (modSupportedExtensionsNode) {
if (modSupportedExtensionsNode)
{
std::string strSupportedExtensions = modSupportedExtensionsNode.AsString();
this->supportedExtensions_ = split(strSupportedExtensions,',');
this->supportedExtensions_ = split(strSupportedExtensions, ',');
}
}
@@ -1908,6 +1978,8 @@ json_map::storage_type<Lv2PluginInfo> Lv2PluginInfo::jmap{{
json_map::reference("has_factory_presets", &Lv2PluginInfo::has_factory_presets_),
json_map::reference("modGui", &Lv2PluginInfo::modGui_),
json_map::reference("patchProperties", &Lv2PluginInfo::patchProperties_),
json_map::reference("hasDefaultState", &Lv2PluginInfo::hasDefaultState_),
}};
json_map::storage_type<Lv2PluginClass> Lv2PluginClass::jmap{{
+4 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) 2022 Robin Davies
// Copyright (c) 2025 Robin E. R. Davies
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
@@ -417,6 +417,8 @@ namespace pipedal
std::vector<std::shared_ptr<Lv2PortInfo>> ports_;
std::vector<std::shared_ptr<Lv2PortGroup>> port_groups_;
std::vector<Lv2PatchPropertyInfo> patchProperties_;
bool hasDefaultState_;
bool is_valid_ = false;
PiPedalUI::ptr piPedalUI_;
@@ -450,6 +452,7 @@ namespace pipedal
LV2_PROPERTY_GETSET(hasUnsupportedPatchProperties)
LV2_PROPERTY_GETSET(modGui)
LV2_PROPERTY_GETSET(patchProperties)
LV2_PROPERTY_GETSET(hasDefaultState)
const Lv2PortInfo &getPort(const std::string &symbol)
{
+4 -1
View File
@@ -23,6 +23,7 @@
#include "json.hpp"
#include <sstream>
#include <string.h>
#include "Lv2Log.hpp"
using namespace pipedal;
@@ -83,6 +84,7 @@ static void CheckState(LV2_State_Status status)
case LV2_State_Status::LV2_STATE_SUCCESS:
return;
default:
case LV2_State_Status::LV2_STATE_ERR_UNKNOWN:
lv2Error = "Unknown error.";
break;
case LV2_State_Status::LV2_STATE_ERR_BAD_TYPE:
@@ -126,7 +128,8 @@ Lv2PluginState StateInterface::Save()
}
catch (const std::exception &e)
{
throw std::logic_error(SS("State save failed. " << e.what()));
Lv2Log::debug(SS("State save failed. " << e.what()));
return Lv2PluginState(); // an invalid state.
}
return std::move(callState.state);
+2
View File
@@ -103,6 +103,8 @@ static void setCacheControl(HttpResponse &res, const fs::path &path)
res.set("Cache-Control", "public, max-age=31536000"); // 1 month
auto lastModified = std::filesystem::last_write_time(path);
res.set(HttpField::LastModified, HtmlHelper::timeToHttpDate(lastModified));
// Set ETag for cache validation
res.set("ETag", pipedal::HtmlHelper::generateEtag(path));
}
else
{
+21 -2
View File
@@ -1,15 +1,34 @@
check reload after change of LV2 plugins.
Magic zoom.
request cacheing not working.
MOD ir uses some scheme to limit the minimum buffer size, which is not implemented in Pipedal.
MOD ir layout issues.
MOD gui control should disable, not disappear.
MOD ir font (cooper-hewitt)
detect self-set control values to prevent weird jitter in mod ui levels.
ZAMEq2 Low control not working.
Modgui view should be persistent/per-plugin
Vu Meters, move rendering to background images, in order to reduce layout overhead.
check reload after change of LV2 plugins.
Remove FullScreenIME in PluginControlView.tsx
Exactly one underrun per seek in Toob Player
Test autohotspot detection.
Ellipsis on plugin Title
check filetime conversion in gcc 12.2! (missing c++ 20 time conversion functions)
AudioFiles.cpp fileTimeToInt64
+114 -1
View File
@@ -1,3 +1,44 @@
@font-face {
font-family: 'cooper hewitt';
src: url('../fonts/cooper/cooperhewitt-light-webfont.woff2') format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap
}
@font-face {
font-family: 'cooper hewitt';
src: url('../fonts/cooper/cooperhewitt-lightitalic-webfont.woff2') format('woff2');
font-weight: 300;
font-style: italic;
font-display: swap
}
@font-face {
font-family: 'cooper hewitt';
src: url('../fonts/cooper/cooperhewitt-bookitalic-webfont.woff2') format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap
}
@font-face {
font-family: 'cooper hewitt';
src: url('../fonts/cooper/cooperhewitt-book-webfont.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap
}
@font-face {
font-family: 'cooper hewitt';
src: url('../fonts/cooper/cooperhewitt-semibold-webfont.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap
}
.mod-pedal {
}
@@ -40,6 +81,11 @@
margin-bottom: 20px;
}
.mod-pedal-settings .mod-control-group {
margin:0;
padding:0 18px;
}
.ppmod-dial-control-image {
width: 48px;
height: 48px;
@@ -61,4 +107,71 @@
white-space: nowrap;
overflow: hidden;
}
.mod-pedal .mod-drag-handle {
bottom:0;
left:0;
right:0;
top:0;
cursor:default; /* we dont do drag */
position:absolute;
z-index: 0;
}
.mod-pedal .mod-control-group {
z-index: 20;
}
.mod-pedal .mod-pedal-input,
.mod-pedal .mod-pedal-output {
z-index:30;
}
/* mod clearfix hack */
.mod-pedal .clearfix:before,
.mod-pedal .clearfix:after {
content: ".";
display: block;
height: 0;
overflow: hidden;
}
.mod-pedal.clearfix:after { clear: both; }
/* enumeration list entries for embedded property browsers. */
.ppmod-dotdot-flex {
display: flex;
align-items: center;
flex-flow: row nowrap;
}
.ppmod-dotdot-text {
white-space: nowrap;
flex: 1 0 auto;
font-size: 0.8em;
padding-right: 8px;
}
.ppmod-dotdot-path {
white-space: nowrap;
font-size: 0.8em;
flex: 0 1 auto;
overflow: hidden;
}
.ppmod-dotdot-breadcrumb_link {
display: inline;
white-space: nowrap;
text-decoration: underline
}
.ppmod-dotdot-breadcrumb_current {
display: inline;
white-space: nowrap;
}
+1 -1
View File
@@ -813,7 +813,7 @@ export
{(!this.state.tinyToolBar) && !this.state.performanceView ?
(
<AppBar position="absolute" >
<Toolbar variant="dense" className={classes.toolBar} >
<Toolbar variant="dense" className={classes.toolBar} style={{overflow: "clip" }} >
<IconButtonEx tooltip="Menu"
edge="start"
aria-label="menu"
+368 -21
View File
@@ -1,23 +1,370 @@
/*
* Copyright (c) 2025 Robin E. R. Davies
* All rights reserved.
// Copyright (c) 2025 Robin E. R. Davies
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
import { Theme } from '@mui/material/styles';
import WithStyles from './WithStyles';
import { withStyles } from "tss-react/mui";
import { createStyles } from './WithStyles';
import FullscreenIcon from '@mui/icons-material/Fullscreen';
import IconButtonEx from './IconButtonEx';
import { isDarkMode } from './DarkMode';
import Backdrop from '@mui/material/Backdrop';
import CloseIcon from '@mui/icons-material/Close';
import Rect from './Rect';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
const styles = (theme: Theme) => createStyles({
pgraph: {
paddingBottom: 16
}
});
export interface AutoZoomProps extends WithStyles<typeof styles> {
leftPad: number,
rightPad: number,
children?: React.ReactNode,
contentReady: boolean,
showZoomed: boolean,
setShowZoomed: (value: boolean) => void;
}
export interface AutoZoomState {
screenWidth: number,
screenHeight: number,
}
const AutoZoom = withStyles(
class extends ResizeResponsiveComponent<AutoZoomProps, AutoZoomState> {
private model: PiPedalModel;
constructor(props: AutoZoomProps) {
super(props);
this.model = PiPedalModelFactory.getInstance();
void this.model; // suppress unused variable warning
this.state = {
screenWidth: this.windowSize.width,
screenHeight: this.windowSize.height,
};
}
mounted: boolean = false;
onWindowSizeChanged(width: number, height: number): void {
this.setState({ screenWidth: width, screenHeight: height });
}
componentDidMount() {
super.componentDidMount();
this.mounted = true;
if (this.normalRef) {
this.updateZoom(this.normalRef);
}
}
componentWillUnmount() {
this.mounted = false;
this.cancelZoomUpdate();
this.cancelMaximizedZoomUpdate();
super.componentWillUnmount();
}
componentDidUpdate(prevProps: AutoZoomProps) {
if (prevProps.contentReady !== this.props.contentReady) {
if (this.normalRef) {
this.updateZoom(this.normalRef);
}
if (this.maximizedRef) {
this.updateMaximizedZoom();
}
}
}
resizeObserver: ResizeObserver | null = null;
calculateZoomedClientRect(
clientRect: Rect,
contentWidth: number,
contentHeight: number,
buttonPadding: number
): Rect {
let rightButtonZoom = Math.min((clientRect.width - buttonPadding) / contentWidth, clientRect.height / contentHeight);
let topButtonZoom = Math.min((clientRect.width - buttonPadding) / contentWidth, (clientRect.height - buttonPadding) / contentHeight);
let effectiveClientRect = clientRect.copy();
if (rightButtonZoom > topButtonZoom) {
effectiveClientRect.width -= buttonPadding;
} else {
effectiveClientRect.y += buttonPadding;
effectiveClientRect.height -= buttonPadding;
}
let zoom = Math.min(effectiveClientRect.width / contentWidth, effectiveClientRect.height / contentHeight);
if (zoom > 1) {
zoom = 1;
}
let leftPad = (effectiveClientRect.width - contentWidth * zoom) / 2;
if (leftPad < 0) {
leftPad = 0;
}
let topPad = (effectiveClientRect.height - contentHeight * zoom) / 4;
if (topPad < 0) {
topPad = 0;
}
return new Rect(effectiveClientRect.x + leftPad, effectiveClientRect.y + topPad, contentWidth * zoom, contentHeight * zoom);
}
updateZoom(ref: HTMLDivElement) {
if (!this.mounted || !ref) {
return;
}
let content = ref.firstElementChild as HTMLElement | null;
if (content) {
const { leftPad, rightPad } = this.props;
let bounds = new Rect(leftPad, 0, ref.clientWidth - leftPad - rightPad, ref.clientHeight - 8);
let zoomedClientRect = this.calculateZoomedClientRect(
bounds, content.clientWidth, content.clientHeight, 48);
content.style.left = "0px";
content.style.top = "0px";
content.style.transformOrigin = "0 0"; // top-left corner
content.style.transform =
`translate(${zoomedClientRect.x}px, ${zoomedClientRect.y}px) scale(${zoomedClientRect.width / content.clientWidth}, ${zoomedClientRect.height / content.clientHeight})`;
if (this.zoomButton) {
this.zoomButton.style.display = zoomedClientRect.width < content.clientWidth ? "block" : "none";
let rightSizeSpace = ref.clientWidth - zoomedClientRect.right;
if (rightSizeSpace - rightPad >= 48) {
this.zoomButton.style.top = `${zoomedClientRect.y + 24}px`;
this.zoomButton.style.left = `${zoomedClientRect.right + 4}px`;
} else {
this.zoomButton.style.top = `${zoomedClientRect.y - 48}px`;
this.zoomButton.style.left = `${zoomedClientRect.right - 64 - 12}px`;
}
}
// save the position of the Mod UI in page coordinates for later zooming.
this.zoomStartRect = zoomedClientRect.copy();
let refBounds = ref.getBoundingClientRect();
this.zoomStartRect.x += refBounds.left;
this.zoomStartRect.y += refBounds.top;
}
}
updateMaximizedZoom() {
let ref = this.maximizedRef;
if (!this.mounted || !ref) {
return;
}
let content = ref.firstElementChild as HTMLElement | null;
if (content) {
let leftPad = 16; let rightPad = 16;
let topPad = 16, bottomPad = 16;
let bounds: Rect;
bounds = new Rect(leftPad, topPad, ref.clientWidth - leftPad - rightPad, ref.clientHeight - topPad - bottomPad);
let zoomedClientRect = this.calculateZoomedClientRect(
bounds, content.clientWidth, content.clientHeight, 48);
content.style.left = "0px";
content.style.top = "0px";
content.style.transformOrigin = "0 0"; // top-left corner
content.style.transform = `translate(${zoomedClientRect.x}px, ${zoomedClientRect.y}px) scale(${zoomedClientRect.width / content.clientWidth}, ${zoomedClientRect.height / content.clientHeight})`;
//console.log("Zoomed to: ", zoomedClientRect.toString());
}
}
private animateFrameHandle: number | null = null;
cancelZoomUpdate() {
if (this.animateFrameHandle) {
window.cancelAnimationFrame(this.animateFrameHandle);
this.animateFrameHandle = null;
}
}
requestZoomUpdate() {
if (this.animateFrameHandle) {
return; // already scheduled
}
this.animateFrameHandle = window.requestAnimationFrame(() => {
this.animateFrameHandle = null;
if (this.normalRef) {
this.updateZoom(this.normalRef);
}
});
}
private maximizedAnimateFrameHandle: number | null = null;
cancelMaximizedZoomUpdate() {
if (this.maximizedAnimateFrameHandle) {
window.cancelAnimationFrame(this.maximizedAnimateFrameHandle);
this.maximizedAnimateFrameHandle = null;
}
}
requestMaximizedZoomUpdate() {
if (this.maximizedAnimateFrameHandle) {
return; // already scheduled
}
this.maximizedAnimateFrameHandle = window.requestAnimationFrame(() => {
this.maximizedAnimateFrameHandle = null;
if (this.maximizedRef) {
this.updateMaximizedZoom();
}
});
}
private zoomButton: HTMLElement | null = null;
private normalRef: HTMLDivElement | null = null;
setNormalRef(ref: HTMLDivElement | null) {
this.normalRef = ref;
if (ref) {
this.zoomButton = ref.querySelector("#maximize-button") as HTMLDivElement | null;
this.resizeObserver = new ResizeObserver(() => {
this.requestZoomUpdate();
});
this.resizeObserver.observe(ref);
let child = ref.firstElementChild as HTMLElement | null;
if (child) {
this.resizeObserver.observe(child);
}
this.requestZoomUpdate();
} else {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
this.zoomButton = null;
}
this.zoomStartRect = null;
}
}
private maximizedRef: HTMLDivElement | null = null;
private maximedResizeObserver: ResizeObserver | null = null;
setMaximizedRef(ref: HTMLDivElement | null) {
this.maximizedRef = ref;
if (ref) {
this.maximedResizeObserver = new ResizeObserver(() => {
this.requestMaximizedZoomUpdate();
});
this.maximedResizeObserver.observe(ref);
let child = ref.firstElementChild as HTMLElement | null;
if (child) {
this.maximedResizeObserver.observe(child);
}
this.requestMaximizedZoomUpdate();
} else {
if (this.maximedResizeObserver) {
this.maximedResizeObserver.disconnect();
this.maximedResizeObserver = null;
}
}
}
private zoomStartRect: Rect | null = null;
startZoom() {
if (!this.zoomStartRect) {
return;
}
this.props.setShowZoomed(true);
}
render() {
const classes = withStyles.getClasses(this.props);
void classes; // suppress unused variable warning
return (<div style={{ overflow: "hidden", height: "100%", width: "100%" }} ref={(ref) => { this.setNormalRef(ref); }}>
<div style={{
display: "inline-block",
visibility: this.props.contentReady ? "visible" : "hidden",
position: "relative",
}}>
{!this.props.showZoomed && this.props.children}
</div>
<div id="maximize-button" style={{
display: "none", position: "absolute", left: 0, top: 0, width: 52, height: 64, zIndex: 1101,
borderRadius: "0px 26px 26px 0px",
}}
>
<IconButtonEx tooltip="Fullscreen view"
style={{ margin: 2, background: isDarkMode() ? "#444" : "#eee" }}
onClick={() => {
this.startZoom();
}}
>
<FullscreenIcon style={{ color: "white" }} />
</IconButtonEx>
</div>
{this.props.showZoomed && (
<Backdrop id="modGuiZoomBackdrop"
sx={(theme) => ({
zIndex: this.props.showZoomed ? 1101 : -1,
overflow: "clip",
background: theme.palette.mode === 'dark' ? 'rgba(0, 0, 0, 0.9)' : 'rgba(255, 255, 255, 0.9)'
})}
open={this.props.showZoomed}
>
<div ref={(ref) => { this.setMaximizedRef(ref); }}
style={{
position: "absolute", left: 0, top: 0, right: 0, bottom: 0, overflow: "hidden",
visibility: this.props.contentReady ? "visible" : "hidden"
}}>
<div style={{ display: "inline-block", position: "relative" }}>
{
this.props.showZoomed && this.props.children
}
</div>
<IconButtonEx tooltip="Exit fullscreen"
style={{
position: "absolute", right: 16, top: 16, zIndex: 1104,
}}
onClick={(e) => {
e.stopPropagation();
this.props.setShowZoomed(false);
}}
>
<CloseIcon />
</IconButtonEx>
</div>
</Backdrop>
)}
</div>);
}
},
styles);
export default AutoZoom;
-4
View File
@@ -63,10 +63,6 @@ function FontTest() {
{TypeSample({ fontFamily: "Pirulen", cssRef: "/fonts/pirulen/stylesheet.css", fontWeights: [400] })}
{TypeSample({ fontFamily: "Questrial", cssRef: "/fonts/questrial/stylesheet.css", fontWeights: [400] })}
{TypeSample({ fontFamily: "Pirulen", cssRef: "/fonts/pirulen/stylesheet.css", fontWeights: [400] })}
{TypeSample({ fontFamily: "Pirulen", cssRef: "/fonts/pirulen/stylesheet.css", fontWeights: [400] })}
{TypeSample({ fontFamily: "Pirulen", cssRef: "/fonts/pirulen/stylesheet.css", fontWeights: [400] })}
</div>
);
+3
View File
@@ -110,6 +110,9 @@ const GxTunerView =
item={this.props.item}
customization={this}
customizationId={this.customizationId}
showModGui={false}
onSetShowModGui= {(instanceId: number, showModGui: boolean) => {}}
/>);
}
},
+146 -50
View File
@@ -20,118 +20,214 @@
// Utility class for constructing Json atoms.
class JsonAtom {
static Property(value: string): any
static _Bool(value: boolean): any
{
return {
"otype_": "Property",
"value": value
};
}
static Bool(value: boolean): any
{
return {
return new JsonAtom({
"otype_": "Bool",
"value": value
};
});
}
static Int(value: number): any
isBool(): boolean
{
return {
return this.isObject() && this.json.otype_ === "Bool";
}
asBool(): boolean {
if (this.isBool()) {
return this.json.value as boolean;
}
throw new Error("JsonAtom is not a Bool");
}
static _Int(value: number): JsonAtom
{
return new JsonAtom({
"otype_": "Int",
"value": value
};
});
}
static Long(value: number): any
isInt(): boolean
{
return {
return this.isObject() && this.json.otype_ === "Int";
}
asInt(): number {
if (this.isInt()) {
return this.json.value as number;;
}
throw new Error("JsonAtom is not an Int");
}
static _Long(value: number): JsonAtom
{
return new JsonAtom({
"otype_": "Long",
"value": value
};
});
}
static Float(value: number): any
isLong(): boolean
{
return value;
return this.isObject() && this.json.otype_ === "Long";
}
static Double(value: number): any
asLong(): number {
if (this.isLong()) {
return this.json.value as number;
}
throw new Error("JsonAtom is not a Long");
}
static _Float(value: number): JsonAtom
{
return {
return new JsonAtom(value);
}
static _Double(value: number): JsonAtom
{
return new JsonAtom({
"otype_": "Double",
"value": value
};
});
}
static String(value: string): any
static _String(value: string): JsonAtom
{
return value;
return new JsonAtom(value);
}
static Path(value: string): any
isString() : boolean
{
return {
return this.json && typeof this.json === 'string';
}
asString(): string {
if (this.isString()) {
return this.json as string;
}
throw new Error("JsonAtom is not a String");
}
static _Path(value: string): JsonAtom
{
return new JsonAtom({
"otype_": "Path",
"value": value
};
});
}
static Uri(value: string): any
isPath(): boolean
{
return {
return this.isObject() && this.json.otype_ === "Path";
}
asPath(): string {
if (this.isPath()) {
return this.json.value as string;
}
throw new Error("JsonAtom is not a Path");
}
static _Uri(value: string): JsonAtom
{
return new JsonAtom({
"otype_": "URI",
"value": value
};
});
}
static Urid(value: string): any
isUri(): boolean
{
return {
return this.isObject() && this.json.otype_ === "URI";
}
asUri(): string {
if (this.isUri()) {
return this.json.value as string;
}
throw new Error("JsonAtom is not a URI");
}
static _Urid(value: string): JsonAtom
{
return new JsonAtom({
"otype_": "URID",
"value": value
};
});
}
static Object(type: string): any
isUrid(): boolean
{
return {
return this.isObject() && this.json.otype_ === "URID";
}
asUrid(): string {
if (this.isUrid()) {
return this.json.value as string;
}
throw new Error("JsonAtom is not a URID");
}
static _Object(type: string): JsonAtom
{
return new JsonAtom({
"otype_": type
};
});
}
static Tuple(values: any[]): any
isObject(type?: string): boolean
{
return {
if (this.json && typeof this.json === 'object' && !Array.isArray(this.json)) {
if (!type) return true;
return type === this.json.otype_;
}
return false;
}
asObject(type?: string): Object {
if (this.isObject(type)) {
return this.json as Object;
}
throw new Error(`JsonAtom is not an Object of type ${type}`);
}
static _Tuple(values: any[]): JsonAtom
{
return new JsonAtom({
"otype_": "Tuple",
"value": values
};
});
}
static IntVector(values: []): any
static _IntVector(values: []): JsonAtom
{
return {
return new JsonAtom({
"otype_": "Vector",
"vtype_": "Int",
"value": values
};
});
}
static LongVector(values: []): any
static _LongVector(values: []): JsonAtom
{
return {
return new JsonAtom({
"otype_": "Vector",
"vtype_": "Long",
"value": values
};
});
}
static FloatVector(values: []): any
static _FloatVector(values: []): JsonAtom
{
return {
return new JsonAtom({
"otype_": "Vector",
"vtype_": "Float",
"value": values
};
});
}
static DoubleVector(values: []): any
static _DoubleVector(values: []): JsonAtom
{
return {
return new JsonAtom({
"otype_": "Vector",
"vtype_": "Float",
"value": values
};
});
}
constructor(json: any) {
this.json = json;
}
isArray(): boolean {
return this.json && Array.isArray(this.json);
}
isFloat(): boolean {
return this.json && typeof this.json === 'number';
}
isNull(): boolean {
return this.json === null;
}
asAny(): any {
return this.json;
}
private json: any;
};
export default JsonAtom;
+11
View File
@@ -1021,6 +1021,17 @@ export class UiPlugin implements Deserializable<UiPlugin> {
return null;
}
getFilePropertyByUri(patchPropertyUri: string): UiFileProperty | null {
for (let i = 0; i < this.fileProperties.length; ++i)
{
let fileProperty = this.fileProperties[i];
if (fileProperty.patchProperty === patchPropertyUri) {
return fileProperty;
}
}
return null;
}
uri: string = "";
name: string = "";
minorVersion: number = 0;
+56 -22
View File
@@ -63,6 +63,7 @@ import PipedalUiIcon from './svg/pp_ui.svg?react';
import SnapshotDialog from './SnapshotDialog';
import { css } from '@emotion/react';
import { setDefaultModGuiPreference } from './ModGuiHost';
const SPLIT_CONTROLBAR_THRESHHOLD = 750;
@@ -253,6 +254,7 @@ export const MainPage =
this.setState({
pedalboard: value,
selectedPedal: selectedItem,
showModUi: value.maybeGetItem(selectedItem)?.useModUi ?? false,
selectedSnapshot: value.selectedSnapshot
});
}
@@ -279,6 +281,14 @@ export const MainPage =
this.model.pedalboard.removeOnChangedHandler(this.onPedalboardChanged);
super.componentWillUnmount();
}
componentDidUpdate(prevProps: MainProps, prevState: MainState) {
if (prevState.selectedPedal !== this.state.selectedPedal
|| prevState.pedalboard !== this.state.pedalboard
) {
this.setState({showModUi: this.state.pedalboard.maybeGetItem(this.state.selectedPedal)?.useModUi??false});
}
}
updateResponsive() {
this.setState({
splitControlBar: this.getSplitToolbar(),
@@ -369,6 +379,15 @@ export const MainPage =
}
handleShowModUi() {
let newState = !this.state.showModUi;
this.model.setPedalboardItemUseModUi(this.state.selectedPedal, newState);
let item = this.model.pedalboard.get().maybeGetItem(this.state.selectedPedal);
if (item) {
setDefaultModGuiPreference(item.uri, newState);
}
}
onPedalboardPropertyChanged(instanceId: number, key: string, value: number) {
this.model.setPedalboardControl(instanceId, key, value);
}
@@ -380,13 +399,14 @@ export const MainPage =
if (pedalboardItem === null) return "";
return pedalboardItem.uri;
}
titleBar(pedalboardItem: PedalboardItem | null,canShowModUi: boolean): React.ReactNode {
titleBar(pedalboardItem: PedalboardItem | null, canShowModUi: boolean): React.ReactNode {
let title = "";
let author = "";
let infoPluginUri = "";
let presetsUri = "";
let missing = false;
let canEditTitle = false;
let modGuiButtonVisible = false;
if (pedalboardItem) {
if (pedalboardItem.isEmpty()) {
title = "";
@@ -416,6 +436,7 @@ export const MainPage =
// if (uiPlugin.description.length > 20) {
// }
infoPluginUri = uiPlugin.uri;
modGuiButtonVisible = true;
}
}
}
@@ -442,20 +463,25 @@ export const MainPage =
display: "flex", flexDirection: "row", height: 48, flexWrap: "nowrap",
alignItems: "center"
}}>
<div style={{ position: "relative", flex: "0 1 auto", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>
<div style={{ position: "relative", flex: "0 1 auto", textAlign: "left", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>
{canEditTitle ? (
<ButtonBase
style={{ borderRadius: "6px", width: "100%", paddingLeft: 8, paddingRight: 8, height: 48, textTransform: "none" }}
style={{
flex: "0 1 auto", borderRadius: "6px", width: "100%", paddingLeft: 8, paddingRight: 8, height: 48,
textTransform: "none", textAlign: "left", justifyItems: "start", overflow: "clip"
}}
onClick={() => {
this.handleEditPluginDisplayName();
}}
>
<span>
<span className={classes.title}>{title}</span>
{this.state.displayAuthor && (
<span className={classes.author}>{author}</span>
)}
</span>
<div style={{ flex: "0 1 auto" }}>
<span>
<span className={classes.title}>{title}</span>
{this.state.displayAuthor && (
<span className={classes.author}>{author}</span>
)}
</span>
</div>
</ButtonBase>)
: (
<div style={{ flex: "0 1 auto", paddingLeft: 8, paddingRight: 8, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>
@@ -474,22 +500,28 @@ export const MainPage =
<PluginPresetSelector pluginUri={presetsUri} instanceId={pedalboardItem?.instanceId ?? 0}
/>
</div>
{canShowModUi && (
<div style={{ flex: "0 0 auot" }}>
<IconButtonEx tooltip={(
<div>
<Typography variant="body2" >
{this.state.showModUi ? "PiPedal UI" : "MOD UI"}</Typography>
<Divider />
<Typography variant="caption">Use MOD UI or PiPedal UI for plugin</Typography>
</div>
)}
{modGuiButtonVisible && (
<div style={{ flex: "0 0 auto" }}>
<IconButtonEx
style={{ opacity: canShowModUi ? 1.0 : 0.4 }}
disabled={!canShowModUi}
tooltip={(
<div>
<Typography variant="body2" >
{this.state.showModUi ? "PiPedal UI" : "MOD UI"}</Typography>
<Divider />
<Typography variant="caption">Use MOD UI or PiPedal UI for plugin.
{!canShowModUi && " The current plugin does not provide a MOD user interface."}
</Typography>
</div>
)}
onClick={() => {
this.setState({ showModUi: !this.state.showModUi });
this.handleShowModUi()
}}
size="large">
{!this.state.showModUi ?
{(!this.state.showModUi || !canShowModUi) ?
(
<ModUiIcon style={{ height: 24, width: 24, fill: this.props.theme.palette.text.primary, opacity: 0.6 }} />
) : (
@@ -498,6 +530,7 @@ export const MainPage =
}
</IconButtonEx>
</div>
)}
</div>
@@ -698,8 +731,9 @@ export const MainPage =
) :
(
GetControlView(pedalboardItem,this.state.showModUi && canShowModUi,
GetControlView(pedalboardItem, this.state.showModUi && canShowModUi,
(instanceId: number, showModGui: boolean) => {
this.model.setPedalboardItemUseModUi(instanceId, showModGui)
this.setState({ showModUi: showModGui });
}
)
+448 -151
View File
@@ -21,17 +21,21 @@
* SOFTWARE.
*/
import { UiPlugin, UiControl, ControlType } from './Lv2Plugin';
import { UiPlugin, UiControl, ControlType, UiFileProperty } from './Lv2Plugin';
import React from 'react';
import CloseIcon from '@mui/icons-material/Close';
import IconButtonEx from './IconButtonEx';
import Typography from '@mui/material/Typography/Typography';
import ModGuiErrorBoundary from './ModGuiErrorBoundary';
import OkDialog from './OkDialog';
import {pathParentDirectory} from './FileUtils';
import { pathFileName, pathParentDirectory } from './FileUtils';
import JsonAtom from './JsonAtom';
import {
PiPedalModel, PiPedalModelFactory, MonitorPortHandle, State,
ListenHandle, FileRequestResult
} from './PiPedalModel';
import { PiPedalModel, PiPedalModelFactory, MonitorPortHandle, State,
ListenHandle, PatchPropertyListener } from './PiPedalModel';
const RANGE_SCALE = 120; // 120 pixels to move from 0 to 1.
const FINE_RANGE_SCALE = RANGE_SCALE * 10; // 1200 pixels to move from 0 to 1.
@@ -41,26 +45,50 @@ const WHEEL_RANGE_SCALE = 120 * 20;
const FINE_WHEEL_RANGE_SCALE = WHEEL_RANGE_SCALE * 10;
const ULTRA_FINE_WHEEL_RANGE_SCALE = WHEEL_RANGE_SCALE * 50;
export interface IModGuiHostSite {
monitorPort: (
instanceId: number,
symbol: string,
interval: number,
callback: (value: number) => void) => MonitorPortHandle;
unmonitorPort: (handle: MonitorPortHandle) => void;
setPedalboardControl: (instanceId: number, symbol: string, value: number) => void;
monitorPatchProperty(
instanceId: number,
propertyUri: string,
onReceived: PatchPropertyListener
): ListenHandle;
export type BypassChangedCallback = (value: boolean) => void;
function HtmlEncode(value: string): string {
value = value.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
return value;
cancelMonitorPatchProperty(listenHandle: ListenHandle): void;
}
function htmlEncodeTest() {
let testString = "&<>'\"\\";
let encoded = HtmlEncode(testString);
let div = document.createElement("div");
div.innerHTML = `<div data-text='${encoded}'>${encoded}</div>`;
let innerDiv = div.children[0] as HTMLDivElement;
if (innerDiv.getAttribute("data-text") !== testString) {
throw new Error("HtmlEncode failed to encode properly: " + encoded);
}
}
htmlEncodeTest();
function PathToString(json: any): string {
let t = new JsonAtom(json);
if (t.isPath()) {
return t.asPath();
}
if (t.isString()) {
return t.asString();
}
return "";
}
function StringToPath(path: string): any {
return JsonAtom._Path(path).asAny();
}
getPatchProperty(instanceId: number, uri: string): Promise<any>;
setPatchProperty(instanceId: number, uri: string, value: any): Promise<boolean>
};
export interface ModGuiHostProps {
instanceId: number,
@@ -69,7 +97,10 @@ export interface ModGuiHostProps {
width?: number;
height?: number;
test?: boolean;
hostSite: IModGuiHostSite;
handleFileSelect: (instanceId: number, propertyUri: string, filePath: string) => void;
onContentReady: (ready: boolean) => void;
}
export interface ModGuiJs {
set_port_value: (symbol: string, value: number) => void;
@@ -92,7 +123,10 @@ interface CustomSelectPathControlProps {
instanceId: number;
plugin: UiPlugin;
propertyUri: string,
hostSite: IModGuiHostSite;
hostSite: PiPedalModel;
handleFileSelect: (instanceId: number, propertyUri: string, filePath: string) => void;
};
@@ -107,6 +141,7 @@ class CustomSelectPathControl implements ModGuiControl {
this.props = props;
}
private isInlineList: boolean = false;
requestUpdate() {
if (!this.mounted) return;
@@ -130,23 +165,149 @@ class CustomSelectPathControl implements ModGuiControl {
}
}
requestFileUpdate() {
private getFileProperty(): UiFileProperty | null {
for (let property of this.props.plugin.fileProperties) {
if (property.patchProperty === this.props.propertyUri) {
return property;
}
}
return null;
}
navBreadcrumbText(breadcrumbs: { pathname: string, displayName: string }[]): string {
let breadcrumbText = "";
for (let i = 0; i < breadcrumbs.length - 1; ++i) {
let breadcrumb = breadcrumbs[i];
breadcrumbText += "<div mod-role='enumeration-option' mod-filetype='directory'"
+ " mod-parameter-value='" + HtmlEncode(breadcrumb.pathname) + "'"
+ " class='ppmod-dotdot-breadcrumb_link'>"
+ HtmlEncode(breadcrumb.displayName)
+ "</div> / ";
}
let breadcrumb = breadcrumbs[breadcrumbs.length - 1];
breadcrumbText += "<div class='ppmod-dotdot-breadcrumb_current'>"
+ HtmlEncode(breadcrumb.displayName)
+ "</div>";
return breadcrumbText;
}
makeEnumeratedListItem(
type: string,
basename: string,
fullname: string,
encodebaseName?: boolean
): HTMLElement {
type = HtmlEncode(type);
fullname = HtmlEncode(fullname);
if (encodebaseName !== false) {
basename = HtmlEncode(basename);
}
let itemHtml = this.enumeratedListItemTemplate
.replace(/{{basename}}/g, basename)
.replace(/{{filetype}}/g, type)
.replace(/{{fullname}}/g, fullname);
let t = document.createElement("div");
t.innerHTML = itemHtml;
let itemElement = t.firstElementChild as HTMLElement;
return itemElement;
}
fileRequestResult: FileRequestResult | null = null;
async requestDirectoryUpdate() {
let model = PiPedalModelFactory.getInstance();
let fileProperty = this.getFileProperty();
if (!fileProperty) {
return;
}
let files = await model.requestFileList2(this.navDirectory || "", fileProperty);
this.fileRequestResult = files;
if (!this.mounted) {
return;
}
if (this.enumeratedListContainerElement) {
this.enumeratedListContainerElement.innerHTML = ""; // remove all children.
if (files.breadcrumbs.length >= 2) {
let parentDirectory = files.breadcrumbs[files.breadcrumbs.length - 2].pathname;
let dotdotHtml = "<div class='ppmod-dotdot-flex'>"
+ "<div mod-role='enumeration-option' mod-filetype='directory'"
+ " mod-parameter-value='" + HtmlEncode(parentDirectory) + "'"
+ " class='ppmod-dotdot-text'>[ ../ ]</div>"
+ "<div class='ppmod-dotdot-path'>"
+ this.navBreadcrumbText(files.breadcrumbs)
+ "</div>";
let dotdotElement = this.makeEnumeratedListItem(
"directory",
dotdotHtml,
parentDirectory,
false
);
dotdotElement.style.setProperty("border-bottom", "1px solid #888");
this.enumeratedListContainerElement.appendChild(dotdotElement);
} else {
// mod-role="enumeration-option" mod-filetype="{{filetype}}" mod-parameter-value="{{fullname}}"
let dotdotHtml = "<div class='ppmod-dotdot-flex'>"
+ "<div class='ppmod-dotdot-text'>&nbsp;</div>"
+ "<div class='ppmod-dotdot-path'>"
+ this.navBreadcrumbText(files.breadcrumbs)
+ "</div>";
let dotdotElement = this.makeEnumeratedListItem(
"directory",
dotdotHtml,
"",
false
);
dotdotElement.style.setProperty("border-bottom", "1px solid #888");
this.enumeratedListContainerElement.appendChild(dotdotElement);
}
for (let file of files.files) {
let displayName = file.displayName;
if (file.isDirectory) {
displayName = '[ ' + displayName + '/ ]';
}
let fileType = file.isDirectory ? "directory" : "file";
let itemElement = this.makeEnumeratedListItem(
fileType,
displayName,
file.pathname
);
this.enumeratedListContainerElement.appendChild(itemElement);
}
this.updateSelection();
}
}
private navDirectory: string | null = null;
setNavDirectory(navDirectory: string | null) {
if (this.navDirectory !== navDirectory) {
this.navDirectory = navDirectory;
this.requestDirectoryUpdate();
}
}
private pathValue: string | null = null;
private browsePath: string | null = null;
setPathValue(value: string) {
if (this.pathValue !== value) {
this.pathValue = value;
if (this.pathValue !== "") {
if (this.pathValue !== "" || this.navDirectory === null) {
let browsePath = pathParentDirectory(value);
if (this.browsePath !== browsePath) {
this.browsePath = browsePath;
this.requestFileUpdate();
}
this.setNavDirectory(browsePath);
}
this.requestUpdate();
}
if (this.enumeratedValueElement) {
this.enumeratedValueElement.textContent = pathFileName(this.pathValue);
}
}
private updateImage() {
@@ -175,7 +336,8 @@ class CustomSelectPathControl implements ModGuiControl {
}
handlePropertyValueChange(value: any) {
let path: string = PathToString(value);
this.setPathValue(path);
}
private mounted: boolean = false;
@@ -183,29 +345,29 @@ class CustomSelectPathControl implements ModGuiControl {
onMounted() {
this.mounted = true;
this.props.hostSite.monitorPatchProperty(this.props.instanceId, this.props.propertyUri,
(value: any) => {
(instanceId, propertyUri, value) => {
this.handlePropertyValueChange(value);
}
);
this.props.hostSite.getPatchProperty(this.props.instanceId, this.props.propertyUri)
.then((value: any) => {
this.handlePropertyValueChange(value);
}).
catch((error: any) => {
if (error instanceof Error) {
console.error((error as Error).message);
} else {
console.error(error.toString());
}
console.error(error);
});
.then((value: any) => {
this.handlePropertyValueChange(value);
}).
catch((error: any) => {
if (error instanceof Error) {
console.error((error as Error).message);
} else {
console.error(error.toString());
}
this.handlePropertyValueChange("");
});
this.requestUpdate();
}
onUnmount() {
if (this.listenHandle) {
this.props.hostSite.cancelMonitorPatchProperty(this.listenHandle);
this.listenHandle = null;
this.listenHandle = null;
}
this.mounted = false;
this.cancelRequestUpdate();
@@ -226,40 +388,92 @@ class CustomSelectPathControl implements ModGuiControl {
}
}
handleSelected(value: string,strFileType: string)
{
this.setPathValue(value);
this.props.hostSite.setPatchProperty(this.props.instanceId, this.props.propertyUri, value);
getValueElement(): HTMLElement | null {
return null;
}
updateSelection() {
if (!this.frameElement) {
return;
}
let valueElement = this.getValueElement();
if (valueElement) {
/// xxx: update the display value.
if (this.pathValue === null || this.pathValue === "") {
valueElement.textContent = "No file selected";
return;
}
valueElement.textContent = this.pathValue;
}
if (this.isInlineList && this.enumeratedListContainerElement) {
for (let i = 0; i < this.enumeratedListContainerElement.children.length; i++) {
let child = this.enumeratedListContainerElement.children[i];
let strValue = child.getAttribute("mod-parameter-value");
let strType = child.getAttribute("mod-filetype");
if (strValue === null) return;
if (strValue === this.pathValue && strType !== "directory") {
child.classList.add("selected");
} else {
child.classList.remove("selected");
}
};
}
}
handleClick(event: MouseEvent) {
if (!this.frameElement) {
return;
}
event.preventDefault();
event.stopPropagation();
let enumeratedList = this.frameElement.querySelector(".mod-enumerated-list");
if (enumeratedList) {
let element = enumeratedList as HTMLElement;
this.toggleVisibility(element)
}
if (event.target) {
let target = event.target as HTMLElement;
let role = target.getAttribute("mod-role");
if (role === "enumeration-option") // a click in the drop-down list?
{
let strValue = target.getAttribute("mod-parameter-value");
let strFileType = target.getAttribute("mod-filetype");
if (strValue && strFileType) {
this.handleSelected(strValue,strFileType);
}
let valueElement = this.getValueElement();
if (event.target === valueElement) {
// A click on the value element, so launch the file browser. hooboy!
return;
}
if (!event.target) {
return;
}
let target = event.target as HTMLElement;
if (target.getAttribute("mod-role") === "enumeration-option") {
let strValue = target.getAttribute("mod-parameter-value");
if (strValue === null) return; let strType = target.getAttribute("mod-filetype");
if (strType === "directory") {
this.setNavDirectory(strValue);
return;
}
this.setPathValue(strValue);
this.updateSelection();
this.props.hostSite.setPatchProperty(this.props.instanceId, this.props.propertyUri, StringToPath(strValue));
return;
}
}
private enumeratedListContainerElement: HTMLElement | null = null;
private enumeratedListItemTemplate: string = "";
private enumeratedValueElement: HTMLElement | null = null;
getValueControlElement(): HTMLElement | null {
if (!this.frameElement) {
return null;
}
return this.frameElement.querySelector('[mod-role=input-parameter-value]') as HTMLElement | null;
}
async handleValueClick(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
this.props.handleFileSelect(
this.props.instanceId,
this.props.propertyUri,
this.pathValue || ""
);
}
attach(frameElement: HTMLElement) {
@@ -280,7 +494,15 @@ class CustomSelectPathControl implements ModGuiControl {
}
}
}
void this.enumeratedListItemTemplate; // suppress unused variable warning
this.enumeratedValueElement = this.getValueControlElement();
this.isInlineList = this.enumeratedValueElement === null;
if (this.enumeratedValueElement) {
this.enumeratedValueElement.onclick = (event: MouseEvent) => {
this.handleValueClick(event);
}
}
}
@@ -362,7 +584,7 @@ class CustomSelectControl implements ModGuiControl {
this.frameElement.querySelectorAll('[mod-role=enumeration-option]')
.forEach((inputControl: Element) => {
let strValue = inputControl.getAttribute("mod-port-value");
let value = parseFloat(strValue || "");
let value = parseFloat(strValue || "");
inputControl.classList.remove("selected");
if (value === this.value) {
inputControl.classList.add("selected");
@@ -431,8 +653,7 @@ class CustomSelectControl implements ModGuiControl {
if (role === "enumeration-option") // a click in the drop-down list?
{
let strValue = target.getAttribute("mod-port-value");
if (strValue && strValue !== "")
{
if (strValue && strValue !== "") {
let value = parseFloat(strValue);
if (!isNaN(value)) {
this.setControlValue(value);
@@ -455,14 +676,7 @@ class CustomSelectControl implements ModGuiControl {
interface BypassLightControlProps {
instanceId: number;
pluginControl: UiControl;
onValueChanged: (instanceId: number, symbol: string, value: number) => void;
monitorPort: (
instanceId: number,
symbol: string,
interval: number,
callback: (value: number) => void) => MonitorPortHandle;
unmonitorPort: (handle: MonitorPortHandle) => void;
model: PiPedalModel;
};
function ModMessage(message: string) {
@@ -517,23 +731,22 @@ class BypassLightControl implements ModGuiControl {
this.frameElement.classList.remove('on', 'off');
this.frameElement.classList.add(
this.value === this.props.pluginControl.min_value ?
this.value ?
'on' : 'off'
);
}
private mounted: boolean = false;
private monitorHandle: MonitorPortHandle | null = null;
private listenHandle: ListenHandle | null = null;
onMounted() {
this.mounted = true;
this.monitorHandle = this.props.monitorPort(
this.listenHandle = this.props.model.addPedalboardItemEnabledChangeListener(
this.props.instanceId,
this.props.pluginControl.symbol,
1.0 / 15,
(value: number) => {
if (value != this.value) {
this.setControlValue(value);
(instanceId: number, isEnabled: boolean) => {
let bypass = isEnabled ? 1.0: 0.0;
if (bypass !== this.value) {
this.setControlValue(bypass);
}
}
);
@@ -541,9 +754,9 @@ class BypassLightControl implements ModGuiControl {
}
onUnmount() {
if (this.monitorHandle) {
this.props.unmonitorPort(this.monitorHandle);
this.monitorHandle = null;
if (this.listenHandle) {
this.props.model.removePedalboardItemEnabledChangeListener(this.listenHandle);
this.listenHandle = null;
}
this.mounted = false;
this.cancelRequestUpdate();
@@ -647,7 +860,7 @@ class FilmstripControl implements ModGuiControl {
return;
}
let range = this.valueToRange(this.value);
this.frameElement.style.transform = `rotate(${rotation * range-rotation/2}deg)`;
this.frameElement.style.transform = `rotate(${rotation * range - rotation / 2}deg)`;
return;
}
@@ -712,9 +925,9 @@ class FilmstripControl implements ModGuiControl {
let value = this.isPointerDown ? this.pointerDownValue : this.value;
value = this.clampValue(value);
let isHorizontalStrip =
this.filmstripInfo.frameWidth/this.filmstripInfo.frameHeight
< this.filmstripInfo.width/this.filmstripInfo.height;
let isHorizontalStrip =
this.filmstripInfo.frameWidth / this.filmstripInfo.frameHeight
< this.filmstripInfo.width / this.filmstripInfo.height;
if (isHorizontalStrip) {
let range = this.valueToRange(value);
@@ -1059,15 +1272,25 @@ class FilmstripControl implements ModGuiControl {
}
};
function ModGuiHost(props: ModGuiHostProps) {
let xmodel: PiPedalModel = PiPedalModelFactory.getInstance();
let model: PiPedalModel = PiPedalModelFactory.getInstance();
const { plugin, onClose } = props;
const [hostDivRef, setHostDivRef] = React.useState<HTMLDivElement | null>(null);
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
const [contentReady, setContentReady] = React.useState<boolean|null>(null);
const [modGuiControls] = React.useState<ModGuiControl[]>([]);
const [ready, setReady] = React.useState<boolean>(xmodel.state.get() === State.Ready);
const [ready, setReady] = React.useState<boolean>(model.state.get() === State.Ready);
const [maximizedUi] = React.useState<boolean>(false);
function updateContentReady(value: boolean) {
if (value !== contentReady) {
setContentReady(value);
props.onContentReady(value);
}
}
if (!plugin.modGui) {
return (
<div>
@@ -1076,6 +1299,7 @@ function ModGuiHost(props: ModGuiHostProps) {
);
}
function addPortClass(element: Element, selector: string, className: string) {
let children = element.querySelectorAll(selector);
// call addClass to each element that matches the selector
@@ -1091,13 +1315,13 @@ function ModGuiHost(props: ModGuiHostProps) {
pluginControl: pluginControl,
onValueChanged: (instanceId: number, symbol: string, value: number) => {
try {
props.hostSite.setPedalboardControl(instanceId, symbol, value);
model.setPedalboardControl(instanceId, symbol, value);
} catch (error) {
}
},
monitorPort: props.hostSite.monitorPort,
unmonitorPort: props.hostSite.unmonitorPort
monitorPort: model.monitorPort.bind(model),
unmonitorPort: model.unmonitorPort.bind(model)
}
);
customSelectControl.attach(control as HTMLElement);
@@ -1108,6 +1332,20 @@ function ModGuiHost(props: ModGuiHostProps) {
}
customSelectControl.onMounted();
}
function monitorBypassPort(
instanceId: number,
symbol: string,
interval: number,
callback: (value: number) => void): MonitorPortHandle {
return model.addPedalboardItemEnabledChangeListener(
instanceId, (instanceId,value) => {
callback(value ? 1 : 0);
}
);
}
function unmonitorBypassPort(handle: MonitorPortHandle) {
model.removePedalboardItemEnabledChangeListener(handle as ListenHandle);
}
function createFootswitchControl(control: Element, symbol: string, controlType: ControlType) {
let uiControl = new UiControl();
uiControl.symbol = symbol;
@@ -1124,14 +1362,24 @@ function ModGuiHost(props: ModGuiHostProps) {
filmStrip: "/img/footswitch_strip.png",
verticalStrip: true,
onValueChanged: (instanceId: number, symbol: string, value: number) => {
try {
props.hostSite.setPedalboardControl(instanceId, symbol, value);
} catch (error) {
if (symbol === "_bypass") {
model.setPedalboardItemEnabled(instanceId, value !== 0);
} else {
try {
model.setPedalboardControl(instanceId, symbol, value);
} catch (error) {
}
}
},
monitorPort: props.hostSite.monitorPort,
unmonitorPort: props.hostSite.unmonitorPort
monitorPort:
symbol === "_bypass" ?
monitorBypassPort :
model.monitorPort.bind(model),
unmonitorPort:
symbol == "_bypass" ?
unmonitorBypassPort :
model.unmonitorPort.bind(model)
}
);
filmstripControl.attach(control as HTMLElement);
@@ -1150,9 +1398,10 @@ function ModGuiHost(props: ModGuiHostProps) {
}
let selectPathControl = new CustomSelectPathControl({
instanceId: props.instanceId,
propertyUri: pathUri,
propertyUri: pathUri,
plugin: props.plugin,
hostSite: props.hostSite
hostSite: model,
handleFileSelect: props.handleFileSelect
});
selectPathControl.attach(control as HTMLElement);
@@ -1160,29 +1409,13 @@ function ModGuiHost(props: ModGuiHostProps) {
selectPathControl.onMounted();
}
function createBypassLightControl(control: Element, symbol: string) {
let uiControl = new UiControl();
uiControl.symbol = symbol;
uiControl.controlType = ControlType.BypassLight;
uiControl.min_value = 0;
uiControl.max_value = 1;
uiControl.is_logarithmic = false;
uiControl.integer_property = true;
function createBypassLightControl(control: Element) {
let bypassLightControl = new BypassLightControl(
{
instanceId: props.instanceId,
pluginControl: uiControl,
onValueChanged: (instanceId: number, symbol: string, value: number) => {
try {
props.hostSite.setPedalboardControl(instanceId, symbol, value);
} catch (error) {
}
},
monitorPort: props.hostSite.monitorPort,
unmonitorPort: props.hostSite.unmonitorPort
model: model
}
);
bypassLightControl.attach(control as HTMLElement);
@@ -1211,13 +1444,13 @@ function ModGuiHost(props: ModGuiHostProps) {
verticalStrip: false,
onValueChanged: (instanceId: number, symbol: string, value: number) => {
try {
props.hostSite.setPedalboardControl(instanceId, symbol, value);
model.setPedalboardControl(instanceId, symbol, value);
} catch (error) {
}
},
monitorPort: props.hostSite.monitorPort,
unmonitorPort: props.hostSite.unmonitorPort
monitorPort: model.monitorPort.bind(model),
unmonitorPort: model.unmonitorPort.bind(model)
}
);
@@ -1284,14 +1517,11 @@ function ModGuiHost(props: ModGuiHostProps) {
.forEach((control) => {
let symbol = "_bypass";
let controlType = ControlType.OnOffSwitch;
createFootswitchControl(control, symbol, controlType);
});
element.querySelectorAll('[mod-role=bypass-light]')
.forEach((control) => {
let symbol = "_bypass";
createBypassLightControl(control, symbol);
createBypassLightControl(control);
});
element.querySelectorAll('[mod-role=input-parameter]')
.forEach((control) => {
@@ -1303,6 +1533,7 @@ function ModGuiHost(props: ModGuiHostProps) {
setErrorMessage(ModMessage(message));
}
async function requestContent() {
updateContentReady(false);
try {
let modGui = plugin.modGui;
if (!modGui) {
@@ -1321,7 +1552,7 @@ function ModGuiHost(props: ModGuiHostProps) {
let encodedUri = encodeURIComponent(props.plugin.uri);
let version = props.plugin.minorVersion * 1000 + props.plugin.microVersion;
let resourceUrl = xmodel.modResourcesUrl;
let resourceUrl = model.modResourcesUrl;
let queryParams = "?ns=" + encodedUri + "&v=" + version.toString();
let templateUri = resourceUrl + "_/iconTemplate" + queryParams;
let cssUri = resourceUrl + "_/stylesheet" + queryParams;
@@ -1367,6 +1598,7 @@ function ModGuiHost(props: ModGuiHostProps) {
let height = element.clientHeight;
hostDivRef.style.width = width + "px";
hostDivRef.style.height = height + "px";
updateContentReady(true);
} catch (error) {
setModError("Error loading UI: " + (error instanceof Error ? error.message : String(error)));
}
@@ -1383,9 +1615,12 @@ function ModGuiHost(props: ModGuiHostProps) {
let stateHandler = (state: State) => {
setReady(state === State.Ready);
}
xmodel.state.addOnChangedHandler(stateHandler)
model.state.addOnChangedHandler(stateHandler)
return () => {
xmodel.state.removeOnChangedHandler(stateHandler);
model.state.removeOnChangedHandler(stateHandler);
if (tHostDivRef !== null) {
// unmount the custom content.
let children = tHostDivRef.children;
@@ -1396,6 +1631,7 @@ function ModGuiHost(props: ModGuiHostProps) {
}
}
}
updateContentReady(false);
let mc = modGuiControls;
for (let i = 0; i < mc.length; i++) {
let modGuiControl = mc[i];
@@ -1405,12 +1641,12 @@ function ModGuiHost(props: ModGuiHostProps) {
};
}, [hostDivRef, plugin, ready]);
return (
<ModGuiErrorBoundary plugin={props.plugin} onClose={() => { props.onClose(); setErrorMessage(null); }}>
<div style={{
position: "absolute", top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: "rgba(0.8,0.8,0.8, 0.8)",
display: "flex", flexFlow: "column nowrap", alignItems: "center", justifyContent: "stretch",
display: "inline-block",
paddingLeft: 20, paddingRight: 20,
overflow: "hidden",
}}
onClick={(event) => {
@@ -1418,14 +1654,14 @@ function ModGuiHost(props: ModGuiHostProps) {
event.stopPropagation();
}}
>
<IconButtonEx tooltip="Close" onClick={() => onClose()} style={{
position: "absolute", top: 16, right: 16,
background: "#FFF5", zIndex: 600
}}>
<CloseIcon />
</IconButtonEx>
<div style={{ flex: "1 1 1px" }} />
{maximizedUi && (
<IconButtonEx tooltip="Close" onClick={() => onClose()} style={{
position: "absolute", top: 16, right: 16,
background: "#FFF5", zIndex: 600
}}>
<CloseIcon />
</IconButtonEx>
)}
<div style={{ display: "block", position: "relative" }}>
<div ref={setHostDivRef} style={{ display: "block", position: "relative" }}
@@ -1437,17 +1673,78 @@ function ModGuiHost(props: ModGuiHostProps) {
/>
</div>
<div style={{ flex: "2 2 1px" }} />
</div>
<OkDialog title="Error" open={errorMessage !== null}
onClose={() => {
setErrorMessage(null);
onClose();
}}
text={errorMessage ?? ""}
/>
{errorMessage !== null && (
<OkDialog title="Error" open={errorMessage !== null}
onClose={() => {
setErrorMessage(null);
onClose();
}}
text={errorMessage ?? ""}
/>
)}
</ModGuiErrorBoundary>
);
}
interface ModGuiPluginPreference {
pluginUri: string;
useModGui: boolean;
}
let modGuiPluginPreferences: ModGuiPluginPreference[] | null = null;
function loadModGuiPreferences() {
if (modGuiPluginPreferences === null) {
let prefs = window.localStorage.getItem("modGuiPluginPreferences");
try {
if (prefs) {
modGuiPluginPreferences = JSON.parse(prefs);
} else {
modGuiPluginPreferences = [];
}
} catch (error) {
console.error("pipedal: Error parsing modGuiPluginPreferences from localStorage: " + String(error));
modGuiPluginPreferences = [];
}
}
}
export function getDefaultModGuiPreference(pluginUri: string) {
loadModGuiPreferences();
if (modGuiPluginPreferences === null) {
modGuiPluginPreferences = [];
}
if (modGuiPluginPreferences) {
let preference = modGuiPluginPreferences.find(p => p.pluginUri === pluginUri);
if (preference) {
return preference.useModGui;
}
}
return false;
}
export function setDefaultModGuiPreference(pluginUri: string, useModGui: boolean) {
loadModGuiPreferences();
if (modGuiPluginPreferences === null) {
modGuiPluginPreferences = [];
}
let preference = modGuiPluginPreferences.find(p => p.pluginUri === pluginUri);
if (preference) {
preference.useModGui = useModGui;
} else {
modGuiPluginPreferences.push({ pluginUri: pluginUri, useModGui });
}
try {
window.localStorage.setItem("modGuiPluginPreferences", JSON.stringify(modGuiPluginPreferences));
} catch(error) {
console.error("pipedal: Error saving modGuiPluginPreferences to localStorage: " + String(error));
}
}
export default ModGuiHost;
-346
View File
@@ -1,346 +0,0 @@
/*
* Copyright (c) 2025 Robin E. R. Davies
* All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from 'react';
import Divider from '@mui/material/Divider';
import { PiPedalModel, PiPedalModelFactory, MonitorPortHandle, State, ListenHandle, PatchPropertyListener } from './PiPedalModel';
import { useTheme } from '@mui/material/styles';
import CircularProgress from '@mui/material/CircularProgress';
import LoadPluginDialog from './LoadPluginDialog';
import ButtonEx from './ButtonEx';
import Typography from '@mui/material/Typography/Typography';
import ModGuiHost, { IModGuiHostSite } from './ModGuiHost';
import { UiPlugin } from './Lv2Plugin';
import ModGuiErrorBoundary from './ModGuiErrorBoundary';
class MyPortHandle implements MonitorPortHandle {
private static nextHandle: number = 1;
constructor() {
this.handle = MyPortHandle.nextHandle++;
}
handle: number;
};
type MonitorCallback = (value: number) => void;
class ValueEntry {
constructor(defaultValue: number) {
this.value = defaultValue;
}
private listeners: { [handle: number]: MonitorCallback } = {};
monitor(interval: number, callback: MonitorCallback): MyPortHandle {
let handle = new MyPortHandle();
this.listeners[handle.handle] = callback;
return handle;
}
unmonitor(handle: MonitorPortHandle) {
let h = (handle as MyPortHandle).handle;
// remove entry h from listeners
if (!(h in this.listeners)) {
throw new Error("Invalid handle: " + h);
}
// remove the callback from the listeners
delete this.listeners[h];
}
setValue(value: number) {
if (value !== this.value) {
this.value = value;
// notify all listeners
for (let handle in this.listeners) {
this.listeners[handle](value);
}
}
}
value: number;
};
class PatchListenerEntry {
constructor(instanceId: number, propertyUri: string) {
this.instanceId = instanceId;
this.propertyUri = propertyUri;
}
instanceId: number;
propertyUri: string;
value: any = "";
listeners: { handle: number, listener: PatchPropertyListener}[] = [];;
};
class ValueHandler {
private valueDictionary: { [symbol: string]: ValueEntry } = {};
private handleToValueEnry: { [handle: number]: ValueEntry } = {};
constructor(instanceId: number, plugin: UiPlugin) {
for (let control of plugin.controls) {
this.valueDictionary[control.symbol] = new ValueEntry(control.default_value);
}
this.valueDictionary["_bypass"] = new ValueEntry(0);
}
monitorPort(instanceId: number, symbol: string, interval: number, callback: MonitorCallback): MonitorPortHandle {
let valueEntry = this.valueDictionary[symbol];
let portHandle = valueEntry.monitor(interval, callback);
this.handleToValueEnry[(portHandle as MyPortHandle).handle] = valueEntry;
callback(valueEntry.value);
return portHandle;
}
unmonitorPort(handle: MonitorPortHandle): void {
let h = (handle as MyPortHandle).handle;
if (!(h in this.handleToValueEnry)) {
throw new Error("Invalid handle: " + h);
}
let valueEntry = this.handleToValueEnry[h];
valueEntry.unmonitor(handle);
delete this.handleToValueEnry[h];
}
setValue(instanceId: number, symbol: string, value: number) {
// add a delay just for funsies.
window.setTimeout(() => {
if (!(symbol in this.valueDictionary)) {
throw new Error("Invalid symbol: " + symbol);
}
let valueEntry = this.valueDictionary[symbol];
valueEntry.setValue(value);
}, 100);
}
getValue(instanceId: number, symbol: string): number {
if (!(symbol in this.valueDictionary)) {
throw new Error("Invalid symbol: " + symbol);
}
let valueEntry = this.valueDictionary[symbol];
return valueEntry.value;
}
private nextListenHandle: number = 1;
private patchListeners: PatchListenerEntry[] = [];
monitorPatchProperty(instanceId: number, propertyUri: string, onReceived: PatchPropertyListener): ListenHandle {
let h = this.nextListenHandle++;
for (let entry of this.patchListeners) {
if (entry.instanceId === instanceId && entry.propertyUri === propertyUri) {
// already listening to this property
entry.listeners.push({ handle: h, listener: onReceived });
onReceived(instanceId,propertyUri,entry.value);
return { _handle: h };
}
}
let entry = new PatchListenerEntry(instanceId, propertyUri);
this.patchListeners.push(entry);
entry.listeners.push({ handle: h, listener: onReceived });
onReceived(instanceId, propertyUri, entry.value);
return {_handle: h};
}
cancelMonitorPatchProperty(listenHandle: ListenHandle): void {
for (let i = 0; i < this.patchListeners.length; ++i) {
let entry = this.patchListeners[i];
for (let j = 0; j < entry.listeners.length; ++j) {
if (entry.listeners[j].handle === listenHandle._handle) {
// found the listener, remove it
entry.listeners.splice(j, 1);
if (entry.listeners.length === 0) {
// no more listeners, remove the entry
this.patchListeners.splice(i, 1);
}
return;
}
}
}
}
getPatchProperty(instanceId: number, uri: string): Promise<any> {
let promise = new Promise<any>((resolve, reject) => {
window.setTimeout(() => {
for (let entry of this.patchListeners) {
if (entry.instanceId === instanceId && entry.propertyUri === uri) {
window.setTimeout(() => {
// simulate a delay for getting the property
entry.listeners.forEach(l => l.listener(instanceId, uri, entry.value));
}, 100);
return resolve(entry.value);
}
}
reject(new Error(`Patch property not found: ${uri} for instance ${instanceId}`));
},50);
});
return promise;
}
setPatchProperty(instanceId: number, uri: string, value: any): Promise<boolean> {
// Implementation here
for (let entry of this.patchListeners) {
if (entry.instanceId === instanceId && entry.propertyUri === uri) {
entry.value = value;
// notify all listeners
entry.listeners.forEach(l => l.listener(instanceId, uri, value));
return Promise.resolve(true);
}
}
// if we reach here, the property was not found
return Promise.reject("Not found.");
}
};
function ModGuiTest() {
const [pluginUrl, setPluginUrl] = React.useState("");
const [uiPlugin, setUiPlugin] = React.useState<UiPlugin | null>(null);
const [pluginDialogOpen, setPluginDialogOpen] = React.useState(false);
const [loading, setLoading] = React.useState(true);
const [valueHandler, setValueHandler] = React.useState<ValueHandler | null>(null);
const [modGuiHostSite] = React.useState<IModGuiHostSite>({
monitorPort: (instanceId: number, symbol: string, interval: number, callback: (value: number) => void) => {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
return valueHandler.monitorPort(instanceId, symbol, interval, callback);
},
unmonitorPort: (handle) => {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
valueHandler.unmonitorPort(handle);
},
setPedalboardControl: (instanceId, symbol, value) => {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
valueHandler.setValue(instanceId, symbol, value);
},
monitorPatchProperty(instanceId: number, propertyUri: string, onReceived: PatchPropertyListener): ListenHandle {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
return valueHandler.monitorPatchProperty(instanceId, propertyUri, onReceived);
},
cancelMonitorPatchProperty: (listenHandle) => {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
valueHandler.cancelMonitorPatchProperty(listenHandle);
},
getPatchProperty: async (instanceId, uri) => {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
return valueHandler.getPatchProperty(instanceId, uri) as Promise<any>;
},
setPatchProperty: async (instanceId, uri, value) => {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
return valueHandler.setPatchProperty(instanceId, uri, value) as Promise<boolean>;
}
});
let model: PiPedalModel = PiPedalModelFactory.getInstance();
React.useEffect(() => {
const onStateChange = (state: State) => {
if (state == State.Ready) {
setLoading(false);
}
};
model.state.addOnChangedHandler(onStateChange);
return () => {
model.state.removeOnChangedHandler(onStateChange);
};
}, [])
const theme = useTheme();
return (
<div style={{
display: "flex", flexFlow: "column nowrap", alignItems: "stretch", justifyContent: "stretch",
position: "absolute", top: 0, left: 0, width: "100%", height: "100%", backgroundColor: (theme.palette as any).mainBackground
}}>
<div style={{
position: "absolute", top: 0, left: 0, right: 0,
bottom: 0, zIndex: 1000,
display: loading ? "flex" : "none",
alignItems: "center", justifyContent: "center",
background: "rgba(0.5,0.5,0.5, 0.9)"
}}>
<CircularProgress style={{ color: theme.palette.text.secondary }} />
</div>
<div style={{
flex: "0 0 auto", display: "flex", flexFlow: "row nowrap",
alignItems: "center", gap: 16, paddingLeft: 32, paddingRight: 32, paddingTop: 16, paddingBottom: 16,
}}>
<Typography noWrap variant="h6" style={{ flex: "1 0 auto" }}>ModGUI Test</Typography>
<Typography noWrap variant="body2" style={{ flex: "0 1 auto", }}>
{pluginUrl ? pluginUrl : "No plugin loaded"}
</Typography>
<ButtonEx style={{ flex: "0 0 auto" }} variant="contained" tooltip="Select plugin" onClick={() => setPluginDialogOpen(true)} >
Load
</ButtonEx>
</div>
<Divider />
{uiPlugin && (
<div style={{ flex: "1 1 auto", position: "relative" }}>
<ModGuiErrorBoundary plugin={uiPlugin} onClose={() => {
setPluginUrl("");
setUiPlugin(null);
setValueHandler(null);
}} >
<ModGuiHost instanceId={-1} plugin={uiPlugin}
onClose={() => {
setPluginUrl("");
setUiPlugin(null);
}}
hostSite={modGuiHostSite}
/>
</ModGuiErrorBoundary>
</div>
)}
{pluginDialogOpen &&
<LoadPluginDialog
open={pluginDialogOpen}
onOk={(url) => {
setPluginUrl(url as string);
let uiPlugin = model.getUiPlugin(url);
setUiPlugin(uiPlugin);
if (uiPlugin) {
setValueHandler(new ValueHandler(-1, uiPlugin));
}
setPluginDialogOpen(false);
}}
onCancel={() => setPluginDialogOpen(false)}
uri={pluginUrl}
modGuiOnly={true}
/>
}
</div>
);
}
export default ModGuiTest;
+3
View File
@@ -79,6 +79,8 @@ export class PedalboardItem implements Deserializable<PedalboardItem> {
this.lv2State = input.lv2State;
this.lilvPresetUri = input.lilvPresetUri;
this.pathProperties = input.pathProperties;
this.useModUi = input.useModUi ?? false;
return this;
}
deserialize(input: any): PedalboardItem {
@@ -212,6 +214,7 @@ export class PedalboardItem implements Deserializable<PedalboardItem> {
lv2State: [boolean,any] = [false,{}];
lilvPresetUri: string = "";
pathProperties: {[Name: string]: string} = {};
useModUi: boolean = false; // true if this item should use the mod-ui.
};
export class SnapshotValue {
+173 -3
View File
@@ -44,7 +44,7 @@ import FilePropertyDirectoryTree from './FilePropertyDirectoryTree';
import AudioFileMetadata from './AudioFileMetadata';
import { pathFileName } from './FileUtils';
import { AlsaSequencerConfiguration, AlsaSequencerPortSelection } from './AlsaSequencer';
import {getDefaultModGuiPreference} from './ModGuiHost';
export enum State {
Loading,
@@ -82,6 +82,24 @@ export enum ReconnectReason {
HotspotChanging
};
export type PedalboardItemEnabledChangeCallback = (instanceId: number, isEnabled: boolean) => void;
interface PedalboardItemEnabledChangeItem {
handle: number;
instanceId: number;
currentValue: boolean;
onEnabledChanged: PedalboardItemEnabledChangeCallback;
};
export type PedalboardItemUseModUiChangeCallback = (instanceId: number, useModUi: boolean) => void;
interface PedalboardItemUseModUiChangeItem {
handle: number;
instanceId: number;
currentValue: boolean;
onUseModUiChanged: PedalboardItemUseModUiChangeCallback;
};
export type ControlValueChangedHandler = (key: string, value: number) => void;
export interface FileEntry {
@@ -146,7 +164,6 @@ export interface VuUpdateInfo {
};
export interface MonitorPortHandle {
};
export interface ControlValueChangedHandle {
_ControlValueChangedHandle: number;
@@ -574,10 +591,18 @@ export class PiPedalModel //implements PiPedalModel
return true;
}
private updateEnabledItems(pedalboard: Pedalboard)
{
for (let item of pedalboard.itemsGenerator()) {
this.updatePedalboardItemEnabled(item.instanceId, item.isEnabled);
this.updatePedalboardItemUseModUi(item.instanceId, item.useModUi);
}
}
private setModelPedalboard(pedalboard: Pedalboard) {
this.pedalboard.set(pedalboard);
this.selectedSnapshot.set(pedalboard.selectedSnapshot);
this.updateEnabledItems(pedalboard);
}
onSocketMessage(header: PiPedalMessageHeader, body?: any) {
@@ -687,6 +712,13 @@ export class PiPedalModel //implements PiPedalModel
itemEnabledBody.enabled,
false // No server notification.
);
} else if (message == "onItemUseModUiChanged") {
let itemEnabledBody = body as PedalboardItemEnableBody;
this._setPedalboardItemUseModUi(
itemEnabledBody.instanceId,
itemEnabledBody.enabled,
false // No server notification.
);
} else if (message === "onPedalboardChanged") {
let pedalChangedBody = body as PedalboardChangedBody;
this.setModelPedalboard(new Pedalboard().deserialize(pedalChangedBody.pedalboard));
@@ -1563,6 +1595,42 @@ export class PiPedalModel //implements PiPedalModel
setPedalboardItemEnabled(instanceId: number, value: boolean): void {
this._setPedalboardItemEnabled(instanceId, value, true);
}
setPedalboardItemUseModUi(instanceId: number, useModUi: boolean): void {
this._setPedalboardItemUseModUi(instanceId, useModUi, true);
}
getPedalboardItemUseModUi(instanceId: number): boolean {
if (!this.pedalboard.get().hasItem(instanceId)) return false;
let item = this.pedalboard.get().getItem(instanceId);
return item.useModUi;
}
_setPedalboardItemUseModUi(instanceId: number, useModUi: boolean, notifyServer: boolean): void {
let pedalboard = this.pedalboard.get();
if (pedalboard === undefined) throw new PiPedalStateError("Pedalboard not ready.");
let newPedalboard = pedalboard.clone();
let item = newPedalboard.getItem(instanceId);
let changed = useModUi !== item.useModUi;
if (changed) {
item.useModUi = useModUi;
this.setModelPedalboard(newPedalboard);
if (notifyServer) {
let body = {
clientId: this.clientId,
instanceId: instanceId,
useModUi: useModUi
};
this.webSocket?.send("setPedalboardItemUseModUi", body);
}
}
}
getPedalboardItemEnabled(instanceId: number): boolean {
if (!this.pedalboard.get().hasItem(instanceId)) return false;
let item = this.pedalboard.get().getItem(instanceId);
return item.isEnabled;
}
private _setPedalboardItemEnabled(instanceId: number, value: boolean, notifyServer: boolean): void {
let pedalboard = this.pedalboard.get();
if (pedalboard === undefined) throw new PiPedalStateError("Pedalboard not ready.");
@@ -1625,6 +1693,7 @@ export class PiPedalModel //implements PiPedalModel
item.lv2State = [false, {}];
item.vstState = "";
item.pathProperties = {};
item.useModUi = getDefaultModGuiPreference(selectedUri);
for (let fileProperty of plugin.fileProperties) {
// stringized json for an atom. see AtomConverter.hpp.
// null -> we've never seen a value.
@@ -1845,6 +1914,7 @@ export class PiPedalModel //implements PiPedalModel
}
newPedalboard.setItemEmpty(item);
this.setModelPedalboard(newPedalboard);
this.updateServerPedalboard();
return item.instanceId;
@@ -2210,6 +2280,18 @@ export class PiPedalModel //implements PiPedalModel
}
getPatchProperty<Type = any>(instanceId: number, uri: string): Promise<Type> {
let result = new Promise<Type>((resolve, reject) => {
let pedalboard = this.pedalboard.get();
if (pedalboard) {
let item = pedalboard.getItem(instanceId);
if (item) {
if (item.pathProperties.hasOwnProperty(uri)) {
let value = item.pathProperties[uri];
let jsonValue = JSON.parse(value);
resolve(jsonValue as Type);
}
}
}
if (!this.webSocket) {
reject("Socket closed.");
} else {
@@ -3233,6 +3315,94 @@ export class PiPedalModel //implements PiPedalModel
throw new Error("No connection.");
}
private pedalboardItemEnabledChangeListeners: PedalboardItemEnabledChangeItem[] = [];
private pedalboardItemUseModUiChangeListeners: PedalboardItemUseModUiChangeItem[] = [];
private updatePedalboardItemEnabled(instanceId: number, enabled: boolean) {
for (let listener of this.pedalboardItemEnabledChangeListeners) {
if (listener.instanceId === instanceId) {
if (listener.currentValue !== enabled) {
listener.currentValue = enabled;
listener.onEnabledChanged(instanceId, enabled);
}
}
}
}
private updatePedalboardItemUseModUi(instanceId: number, useModUi: boolean) {
for (let listener of this.pedalboardItemUseModUiChangeListeners) {
if (listener.instanceId === instanceId) {
if (listener.currentValue !== useModUi) {
listener.currentValue = useModUi;
listener.onUseModUiChanged(instanceId, useModUi);
}
}
}
}
addPedalboardItemEnabledChangeListener(
instanceId: number,
onEnabledChanged: PedalboardItemEnabledChangeCallback
) : ListenHandle{
let handle = ++this.nextListenHandle;
let currentValue = this.getPedalboardItemEnabled(instanceId);
let entry: PedalboardItemEnabledChangeItem = {
instanceId: instanceId,
handle: handle,
currentValue: currentValue,
onEnabledChanged: onEnabledChanged
};
this.pedalboardItemEnabledChangeListeners.push( entry);
onEnabledChanged(instanceId, currentValue);
return { _handle: handle };
}
addPedalboardItemUseModUiChangeListener(
instanceId: number,
onUseModUiChanged : PedalboardItemUseModUiChangeCallback
) : ListenHandle{
let handle = ++this.nextListenHandle;
let currentValue = this.getPedalboardItemUseModUi(instanceId);
let entry: PedalboardItemUseModUiChangeItem = {
instanceId: instanceId,
handle: handle,
currentValue: currentValue,
onUseModUiChanged: onUseModUiChanged
};
this.pedalboardItemUseModUiChangeListeners.push( entry);
onUseModUiChanged(instanceId, currentValue);
return { _handle: handle };
}
removePedalboardItemEnabledChangeListener(listenHandle: ListenHandle): boolean {
for (let i = 0; i < this.pedalboardItemEnabledChangeListeners.length; ++i) {
if (this.pedalboardItemEnabledChangeListeners[i].handle === listenHandle._handle) {
this.pedalboardItemEnabledChangeListeners.splice(i, 1);
return true;
}
}
return false;
}
removePedalboardItemUseModUiChangeListener(listenHandle: ListenHandle): boolean {
for (let i = 0; i < this.pedalboardItemUseModUiChangeListeners.length; ++i) {
if (this.pedalboardItemUseModUiChangeListeners[i].handle === listenHandle._handle) {
this.pedalboardItemUseModUiChangeListeners.splice(i, 1);
return true;
}
}
return false;
}
};
let instance: PiPedalModel | undefined = undefined;
+81 -45
View File
@@ -1,4 +1,4 @@
// Copyright (c) 2022 Robin Davies
// Copyright (c) 2025 Robin E. R. Davies
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
@@ -23,7 +23,9 @@ import WithStyles, { withTheme } from './WithStyles';
import { createStyles } from './WithStyles';
import { css } from '@emotion/react';
import ModGuiHost, { IModGuiHostSite } from './ModGuiHost';
import AutoZoom from './AutoZoom';
import ModGuiHost from './ModGuiHost';
import { withStyles } from "tss-react/mui";
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
@@ -353,7 +355,9 @@ type PluginControlViewState = {
imeInitialHeight: number;
showFileDialog: boolean,
dialogFileProperty: UiFileProperty,
dialogFileValue: string
dialogFileValue: string,
modGuiContentReady: boolean,
showModGuiZoomed: boolean,
};
const PluginControlView =
@@ -372,7 +376,9 @@ const PluginControlView =
imeInitialHeight: 0,
showFileDialog: false,
dialogFileProperty: new UiFileProperty(),
dialogFileValue: ""
dialogFileValue: "",
modGuiContentReady: false,
showModGuiZoomed: false
}
this.onPedalboardChanged = this.onPedalboardChanged.bind(this);
@@ -774,36 +780,64 @@ const PluginControlView =
}
return false;
}
makeHostSite(): IModGuiHostSite {
// Yuck. :-( This worked out badly.
let model = this.model;
return {
monitorPort: this.model.monitorPort.bind(model),
unmonitorPort: this.model.unmonitorPort.bind(model),
setPedalboardControl: this.model.setPedalboardControl.bind(model),
monitorPatchProperty: this.model.monitorPatchProperty.bind(model),
cancelMonitorPatchProperty: this.model.cancelMonitorPatchProperty.bind(model),
getPatchProperty: this.model.getPatchProperty.bind(model),
setPatchProperty: this.model.setPatchProperty.bind(model),
};
}
handleModGuiFileProperty(instanceId: number, filePropertyUri: string, selectedFile: string) {
let plugin = this.model.getUiPlugin(this.props.item.uri);
if (!plugin) {
throw (new Error("Plugin not found."));
}
let fileProperty = plugin.getFilePropertyByUri(filePropertyUri);
if (!fileProperty) {
throw (new Error("File property not found."));
}
this.setState({
showFileDialog: true,
dialogFileProperty: fileProperty,
dialogFileValue: selectedFile
});
}
renderModGui(): ReactNode {
const classes = withStyles.getClasses(this.props);
void classes;
let uiPlugin = this.model.getUiPlugin(this.props.item.uri);
if (!uiPlugin) {
return (<div />);
}
return (
<ModGuiHost
instanceId={this.props.instanceId}
plugin={uiPlugin}
onClose={() => {
if (this.props.onSetShowModGui) {
this.props.onSetShowModGui(this.props.instanceId, false);
<div style={{ width: "100%", height: "100%", }}>
<AutoZoom leftPad={36} rightPad={52} contentReady={this.state.modGuiContentReady}
showZoomed={this.state.showModGuiZoomed}
setShowZoomed={
(value) => { this.setState({ showModGuiZoomed: value }); }
}
}}
hostSite={this.makeHostSite()}
/>
>
<ModGuiHost
key={this.props.instanceId.toString()}
instanceId={this.props.instanceId}
plugin={uiPlugin}
onClose={() => {
if (this.props.onSetShowModGui) {
this.props.onSetShowModGui(this.props.instanceId, false);
}
this.setState({showModGuiZoomed: false})
}}
handleFileSelect={(instanceId, fileProperty, selectedFile) => {
this.handleModGuiFileProperty(
instanceId,
fileProperty,
selectedFile);
}}
onContentReady={(value) => {
this.setState({ modGuiContentReady: value });
}}
/>
</AutoZoom>
</div>
)
}
@@ -859,22 +893,22 @@ const PluginControlView =
nodes.push(this.midiBindingControl(pedalboardItem));
}
return (
<div className={scrollClass}>
<div className={gridClass} >
{
nodes
}
{/* Extra space to allow scrolling right to the end in lascape especially */}
{!this.fullScreen() && (
<div style={{ flex: "0 0 40px", width: 40, height: 40 }} />
)}
{
(!this.state.landscapeGrid) && (!this.fullScreen()) && (
<div style={{ flex: "0 1 100%", width: "0px", height: 40 }} />
)
}
</div>
<div className={scrollClass}>
<div className={gridClass} >
{
nodes
}
{/* Extra space to allow scrolling right to the end in lascape especially */}
{!this.fullScreen() && (
<div style={{ flex: "0 0 40px", width: 40, height: 40 }} />
)}
{
(!this.state.landscapeGrid) && (!this.fullScreen()) && (
<div style={{ flex: "0 1 100%", width: "0px", height: 40 }} />
)
}
</div>
</div>
);
@@ -895,7 +929,7 @@ const PluginControlView =
let vuMeterRClass = this.state.landscapeGrid ? classes.vuMeterRLandscape : classes.vuMeterR;
let frameClass = classes.frame;
if (this.fullScreen()) {
if (this.fullScreen() || this.props.showModGui) {
frameClass = classes.noScrollFrame;
}
@@ -920,13 +954,15 @@ const PluginControlView =
instanceId={this.props.instanceId}
selectedFile={this.state.dialogFileValue}
onCancel={() => {
this.setState({ showFileDialog: false });
this.setState({
showFileDialog: false
});
}}
onApply={(fileProperty, selectedFile) => {
this.model.setPatchProperty(
this.props.instanceId,
fileProperty.patchProperty,
JsonAtom.Path(selectedFile)
JsonAtom._Path(selectedFile).asAny()
)
.then(() => {
@@ -941,7 +977,7 @@ const PluginControlView =
this.model.setPatchProperty(
this.props.instanceId,
fileProperty.patchProperty,
JsonAtom.Path(selectedFile)
JsonAtom._Path(selectedFile).asAny()
)
.then(() => {
+7 -1
View File
@@ -31,7 +31,7 @@ class Rect {
this.x = x; this.y = y; this.width = width; this.height = height;
}
toString() { return '{' + this.x + "," + this.y + " " + this.width + "," + this.height +"}"; }
toString() { return '{' + this.x + "," + this.y + " - " + this.width + "," + this.height +"}"; }
copy(): Rect {
let result = new Rect();
result.x = this.x;
@@ -84,6 +84,12 @@ class Rect {
this.x += xOffset;
this.y += yOffset;
}
equals(other: Rect): boolean {
return this.x === other.x &&
this.y === other.y &&
this.width === other.width &&
this.height === other.height;
}
}
export default Rect;
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) 2022 Robin Davies
// Copyright (c) 2025 Robin E. R. Davies
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
+3
View File
@@ -77,6 +77,9 @@ const ToobCabSimView =
item={this.props.item}
customization={this}
customizationId={this.customizationId}
showModGui={false}
onSetShowModGui= {(instanceId: number, showModGui: boolean) => {}}
/>);
}
},
+9 -8
View File
@@ -21,14 +21,14 @@ import React from 'react';
import { Theme } from '@mui/material/styles';
import WithStyles from './WithStyles';
import {createStyles} from './WithStyles';
import { createStyles } from './WithStyles';
import { withStyles } from "tss-react/mui";
import IControlViewFactory from './IControlViewFactory';
import { PiPedalModelFactory, PiPedalModel } from "./PiPedalModel";
import { PedalboardItem } from './Pedalboard';
import PluginControlView, { ControlGroup,ControlViewCustomization } from './PluginControlView';
import PluginControlView, { ControlGroup, ControlViewCustomization } from './PluginControlView';
//import ToobFrequencyResponseView from './ToobFrequencyResponseView';
@@ -47,12 +47,11 @@ interface ToobInputStageState {
const ToobInputStageView =
withStyles(
class extends React.Component<ToobInputStageProps, ToobInputStageState>
implements ControlViewCustomization
{
class extends React.Component<ToobInputStageProps, ToobInputStageState>
implements ControlViewCustomization {
model: PiPedalModel;
customizationId: number = 1;
customizationId: number = 1;
constructor(props: ToobInputStageProps) {
super(props);
@@ -64,8 +63,7 @@ const ToobInputStageView =
return false;
}
modifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
{
modifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] {
return controls;
// let group = controls[1] as ControlGroup;
// group.controls.splice(0,0,
@@ -79,6 +77,9 @@ const ToobInputStageView =
item={this.props.item}
customization={this}
customizationId={this.customizationId}
showModGui={false}
onSetShowModGui={(instanceId: number, showModGui: boolean) => { }}
/>);
}
},
+10 -9
View File
@@ -21,7 +21,7 @@ import React from 'react';
import { Theme } from '@mui/material/styles';
import WithStyles from './WithStyles';
import {createStyles} from './WithStyles';
import { createStyles } from './WithStyles';
import { withStyles } from "tss-react/mui";
@@ -48,7 +48,7 @@ const ToobMLView =
class extends React.Component<ToobMLProps, ToobMLState>
implements ControlViewCustomization {
model: PiPedalModel;
gainRef: React.RefObject<HTMLDivElement|null>;
gainRef: React.RefObject<HTMLDivElement | null>;
customizationId: number = 1;
@@ -119,19 +119,17 @@ const ToobMLView =
// }
let controlIndex: number = -1;
let controlValues = this.props.item.controlValues
for (let i:number = 0; i < controlValues.length; ++i)
{
for (let i: number = 0; i < controlValues.length; ++i) {
let ctl: any = controls[i];
let key: any = ctl?.props?.uiControl?.symbol??"";
if (key === "gain")
{
let key: any = ctl?.props?.uiControl?.symbol ?? "";
if (key === "gain") {
controlIndex = i;
break;
}
}
if (controlIndex !== -1 ) {
if (controlIndex !== -1) {
let gainControl: React.ReactElement = controls[controlIndex] as React.ReactElement;
controls[controlIndex] = (<div ref={this.gainRef}> {gainControl} </div>);
@@ -144,6 +142,9 @@ const ToobMLView =
item={this.props.item}
customization={this}
customizationId={this.customizationId}
showModGui={false}
onSetShowModGui={(instanceId: number, showModGui: boolean) => { }}
/>);
}
},
+4 -4
View File
@@ -525,7 +525,7 @@ export default function ToobPlayerControl(
function onNextTrack() {
model.getNextAudioFile(audioFile)
.then((file) => {
let json = JsonAtom.Path(file);
let json = JsonAtom._Path(file).asAny();
model.setPatchProperty(
props.instanceId,
AUDIO_FILE_PROPERTY_URI,
@@ -538,7 +538,7 @@ export default function ToobPlayerControl(
function onPreviousTrack() {
model.getPreviousAudioFile(audioFile)
.then((file) => {
let json = JsonAtom.Path(file);
let json = JsonAtom._Path(file).asAny();
model.setPatchProperty(
props.instanceId,
AUDIO_FILE_PROPERTY_URI,
@@ -873,7 +873,7 @@ export default function ToobPlayerControl(
model.setPatchProperty(
props.instanceId,
AUDIO_FILE_PROPERTY_URI,
JsonAtom.Path(selectedFile)
JsonAtom._Path(selectedFile).asAny()
)
.then(() => {
@@ -888,7 +888,7 @@ export default function ToobPlayerControl(
model.setPatchProperty(
props.instanceId,
fileProperty.patchProperty,
JsonAtom.Path(selectedFile)
JsonAtom._Path(selectedFile).asAny()
)
.then(() => {
+6 -3
View File
@@ -110,14 +110,14 @@ const ToobPlayerView =
for (let mixControl of mixPanel.controls) {
extraControls.push(
(
<div key={"kExtra"+ iKey++} style={{flex: "0 0 auto", position: "relative",height: "100%" }}>
<div key={"kExtra" + iKey++} style={{ flex: "0 0 auto", position: "relative", height: "100%" }}>
{mixControl}
</div>
));
}
let panel = (
<ToobPlayerControl key={"MusicPlayer" + this.props.instanceId} instanceId={this.props.instanceId} extraControls={extraControls} />
<ToobPlayerControl key={"MusicPlayer" + this.props.instanceId} instanceId={this.props.instanceId} extraControls={extraControls} />
);
let result: (React.ReactNode | ControlGroup)[] = [];
@@ -133,6 +133,9 @@ const ToobPlayerView =
item={this.props.item}
customization={this}
customizationId={this.customizationId}
showModGui={false}
onSetShowModGui={(instanceId: number, showModGui: boolean) => { }}
/>
);
}
@@ -146,7 +149,7 @@ class ToobPlayerViewFactory implements IControlViewFactory {
uri: string = "http://two-play.com/plugins/toob-player";
Create(model: PiPedalModel, pedalboardItem: PedalboardItem): React.ReactNode {
return (<ToobPlayerView key={"ppmv"+pedalboardItem.instanceId} instanceId={pedalboardItem.instanceId} item={pedalboardItem} />);
return (<ToobPlayerView key={"ppmv" + pedalboardItem.instanceId} instanceId={pedalboardItem.instanceId} item={pedalboardItem} />);
}
+2
View File
@@ -179,6 +179,8 @@ const ToobPowerstage2View =
item={this.props.item}
customization={this}
customizationId={this.customizationId}
showModGui={false}
onSetShowModGui= {(instanceId: number, showModGui: boolean) => {}}
/>);
}
},
+12 -11
View File
@@ -21,14 +21,14 @@ import React from 'react';
import { Theme } from '@mui/material/styles';
import WithStyles from './WithStyles';
import {createStyles} from './WithStyles';
import { createStyles } from './WithStyles';
import { withStyles } from "tss-react/mui";
import IControlViewFactory from './IControlViewFactory';
import { PiPedalModelFactory, PiPedalModel } from "./PiPedalModel";
import { PedalboardItem } from './Pedalboard';
import PluginControlView, { ControlGroup,ControlViewCustomization } from './PluginControlView';
import PluginControlView, { ControlGroup, ControlViewCustomization } from './PluginControlView';
import ToobSpectrumResponseView from './ToobSpectrumResponseView';
@@ -48,12 +48,11 @@ interface ToobSpectrumAnalyzerState {
const ToobSpectrumAnalyzerView =
withStyles(
class extends React.Component<ToobSpectrumAnalyzerProps, ToobSpectrumAnalyzerState>
implements ControlViewCustomization
{
class extends React.Component<ToobSpectrumAnalyzerProps, ToobSpectrumAnalyzerState>
implements ControlViewCustomization {
model: PiPedalModel;
customizationId: number = 1;
customizationId: number = 1;
constructor(props: ToobSpectrumAnalyzerProps) {
super(props);
@@ -65,11 +64,10 @@ const ToobSpectrumAnalyzerView =
return false;
}
modifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
{
controls.splice(0,0,
( <ToobSpectrumResponseView instanceId={this.props.instanceId} />)
);
modifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] {
controls.splice(0, 0,
(<ToobSpectrumResponseView instanceId={this.props.instanceId} />)
);
return controls;
}
render() {
@@ -78,6 +76,9 @@ const ToobSpectrumAnalyzerView =
item={this.props.item}
customization={this}
customizationId={this.customizationId}
showModGui={false}
onSetShowModGui={(instanceId: number, showModGui: boolean) => { }}
/>);
}
},
+24 -27
View File
@@ -21,14 +21,14 @@ import React from 'react';
import { Theme } from '@mui/material/styles';
import WithStyles from './WithStyles';
import {createStyles} from './WithStyles';
import { createStyles } from './WithStyles';
import { withStyles } from "tss-react/mui";
import IControlViewFactory from './IControlViewFactory';
import { PiPedalModelFactory, PiPedalModel,ControlValueChangedHandle } from "./PiPedalModel";
import { PiPedalModelFactory, PiPedalModel, ControlValueChangedHandle } from "./PiPedalModel";
import { PedalboardItem } from './Pedalboard';
import PluginControlView, { ControlGroup,ControlViewCustomization } from './PluginControlView';
import PluginControlView, { ControlGroup, ControlViewCustomization } from './PluginControlView';
import ToobFrequencyResponseView from './ToobFrequencyResponseView';
@@ -47,12 +47,11 @@ interface ToobToneStackState {
const ToobToneStackView =
withStyles(
class extends React.Component<ToobToneStackProps, ToobToneStackState>
implements ControlViewCustomization
{
class extends React.Component<ToobToneStackProps, ToobToneStackState>
implements ControlViewCustomization {
model: PiPedalModel;
customizationId: number = 1;
customizationId: number = 1;
constructor(props: ToobToneStackProps) {
super(props);
@@ -61,27 +60,24 @@ const ToobToneStackView =
isBaxandall: this.IsBaxandall()
}
}
IsBaxandall() : boolean {
IsBaxandall(): boolean {
return this.props.item.getControl("ampmodel").value === 2.0;
}
controlValueChangedHandle?: ControlValueChangedHandle;
componentDidMount()
{
componentDidMount() {
this.controlValueChangedHandle = this.model.addControlValueChangeListener(
this.props.instanceId,
(key,value) => {
if (key === "ampmodel")
{
this.setState({isBaxandall: value === 2.0});
(key, value) => {
if (key === "ampmodel") {
this.setState({ isBaxandall: value === 2.0 });
}
}
);
}
componentWillUnmount() {
if (this.controlValueChangedHandle)
{
if (this.controlValueChangedHandle) {
this.model.removeControlValueChangeListener(this.controlValueChangedHandle);
this.controlValueChangedHandle = undefined;
}
@@ -90,18 +86,16 @@ const ToobToneStackView =
return false;
}
modifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
{
if (this.state.isBaxandall)
{
controls.splice(0,0,
( <ToobFrequencyResponseView instanceId={this.props.instanceId}
/>)
);
modifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] {
if (this.state.isBaxandall) {
controls.splice(0, 0,
(<ToobFrequencyResponseView instanceId={this.props.instanceId}
/>)
);
} else {
controls.splice(0,0,
( <ToobFrequencyResponseView instanceId={this.props.instanceId} />)
);
controls.splice(0, 0,
(<ToobFrequencyResponseView instanceId={this.props.instanceId} />)
);
}
return controls;
}
@@ -111,6 +105,9 @@ const ToobToneStackView =
item={this.props.item}
customization={this}
customizationId={this.customizationId}
showModGui={false}
onSetShowModGui={(instanceId: number, showModGui: boolean) => { }}
/>);
}
},