MOD Gui support.
This commit is contained in:
@@ -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 << "<"; break;
|
||||
case '>': os << ">"; break;
|
||||
case '&': os << "&"; break;
|
||||
default: os << c; break;
|
||||
|
||||
case '<':
|
||||
os << "<";
|
||||
break;
|
||||
case '>':
|
||||
os << ">";
|
||||
break;
|
||||
case '&':
|
||||
os << "&";
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -65,5 +65,8 @@ namespace pipedal {
|
||||
const LV2_URID_Map *GetMap() const { return ↦}
|
||||
LV2_URID_Map *GetMap() { return ↦}
|
||||
|
||||
const LV2_URID_Unmap *GetUnmap() const { return &unmap;}
|
||||
LV2_URID_Unmap *GetUnmap() { return &unmap;}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -110,6 +110,9 @@ const GxTunerView =
|
||||
item={this.props.item}
|
||||
customization={this}
|
||||
customizationId={this.customizationId}
|
||||
showModGui={false}
|
||||
onSetShowModGui= {(instanceId: number, showModGui: boolean) => {}}
|
||||
|
||||
/>);
|
||||
}
|
||||
},
|
||||
|
||||
+146
-50
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
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'> </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;
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -77,6 +77,9 @@ const ToobCabSimView =
|
||||
item={this.props.item}
|
||||
customization={this}
|
||||
customizationId={this.customizationId}
|
||||
showModGui={false}
|
||||
onSetShowModGui= {(instanceId: number, showModGui: boolean) => {}}
|
||||
|
||||
/>);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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) => { }}
|
||||
|
||||
/>);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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) => { }}
|
||||
|
||||
/>);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
|
||||
@@ -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} />);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -179,6 +179,8 @@ const ToobPowerstage2View =
|
||||
item={this.props.item}
|
||||
customization={this}
|
||||
customizationId={this.customizationId}
|
||||
showModGui={false}
|
||||
onSetShowModGui= {(instanceId: number, showModGui: boolean) => {}}
|
||||
/>);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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) => { }}
|
||||
|
||||
/>);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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) => { }}
|
||||
|
||||
/>);
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user