MOD Gui support.
This commit is contained in:
@@ -24,22 +24,20 @@
|
|||||||
|
|
||||||
using namespace pipedal;
|
using namespace pipedal;
|
||||||
|
|
||||||
|
|
||||||
// non-localizable. RFC 7231 Date Format
|
// non-localizable. RFC 7231 Date Format
|
||||||
;
|
;
|
||||||
static const char*INVARIANT_WEEK_DAYS[] =
|
static const char *INVARIANT_WEEK_DAYS[] =
|
||||||
{ "Sun","Mon", "Tue", "Wed","Thu", "Fri","Sat"};
|
{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
|
||||||
// non-localizable. RFC 7231 Date Format
|
// 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
|
// 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()
|
std::string HtmlHelper::timeToHttpDate()
|
||||||
{
|
{
|
||||||
return timeToHttpDate(time(nullptr));
|
return timeToHttpDate(time(nullptr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
std::string HtmlHelper::timeToHttpDate(std::filesystem::file_time_type time)
|
std::string HtmlHelper::timeToHttpDate(std::filesystem::file_time_type time)
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -56,29 +54,30 @@ std::string HtmlHelper::timeToHttpDate(time_t time)
|
|||||||
|
|
||||||
struct tm gmTm;
|
struct tm gmTm;
|
||||||
|
|
||||||
if (gmtime_r(&time,&gmTm) == nullptr)
|
if (gmtime_r(&time, &gmTm) == nullptr)
|
||||||
{
|
{
|
||||||
return "Mon, 01 Jan 1970 00:00:00 GMT";
|
return "Mon, 01 Jan 1970 00:00:00 GMT";
|
||||||
}
|
}
|
||||||
char szBuffer[sizeof("Mon, 01 Jan 1970 00:00:00 GMT+")];
|
char szBuffer[sizeof("Mon, 01 Jan 1970 00:00:00 GMT+")];
|
||||||
snprintf(szBuffer,sizeof(szBuffer), FORMAT,
|
snprintf(szBuffer, sizeof(szBuffer), FORMAT,
|
||||||
INVARIANT_WEEK_DAYS[gmTm.tm_wday],
|
INVARIANT_WEEK_DAYS[gmTm.tm_wday],
|
||||||
gmTm.tm_mday,
|
gmTm.tm_mday,
|
||||||
INVARIANT_MONTHS[gmTm.tm_mon],
|
INVARIANT_MONTHS[gmTm.tm_mon],
|
||||||
gmTm.tm_year+1900,
|
gmTm.tm_year + 1900,
|
||||||
gmTm.tm_hour,
|
gmTm.tm_hour,
|
||||||
gmTm.tm_min,
|
gmTm.tm_min,
|
||||||
gmTm.tm_sec);
|
gmTm.tm_sec);
|
||||||
return szBuffer;
|
return szBuffer;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static int hexN(char c)
|
static int hexN(char c)
|
||||||
{
|
{
|
||||||
if (c >= '0' && c <= '9') return c-'0';
|
if (c >= '0' && c <= '9')
|
||||||
if (c >= 'a' && c <= 'f') return 10+c-'a';
|
return c - '0';
|
||||||
if (c >= 'A' && c <= 'F') return 10+c-'A';
|
if (c >= 'a' && c <= 'f')
|
||||||
|
return 10 + c - 'a';
|
||||||
|
if (c >= 'A' && c <= 'F')
|
||||||
|
return 10 + c - 'A';
|
||||||
throw std::invalid_argument("Malformed URL.");
|
throw std::invalid_argument("Malformed URL.");
|
||||||
}
|
}
|
||||||
static char hexChar(char c1, char c2)
|
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));
|
return (char)((hexN(c1) << 4) + hexN(c2));
|
||||||
}
|
}
|
||||||
|
|
||||||
class SafeCharacterMap {
|
class SafeCharacterMap
|
||||||
|
{
|
||||||
bool safeCharacter[256];
|
bool safeCharacter[256];
|
||||||
|
|
||||||
public:
|
public:
|
||||||
SafeCharacterMap()
|
SafeCharacterMap()
|
||||||
{
|
{
|
||||||
for (int i = 0; i < 0x20; ++i) {
|
for (int i = 0; i < 0x20; ++i)
|
||||||
|
{
|
||||||
safeCharacter[i] = false;
|
safeCharacter[i] = false;
|
||||||
}
|
}
|
||||||
for (int i = 0x20; i < 0x80; ++i) {
|
for (int i = 0x20; i < 0x80; ++i)
|
||||||
|
{
|
||||||
safeCharacter[i] = true;
|
safeCharacter[i] = true;
|
||||||
}
|
}
|
||||||
for (int i = 0x80; i < 256; ++i)
|
for (int i = 0x80; i < 256; ++i)
|
||||||
@@ -102,27 +105,25 @@ public:
|
|||||||
safeCharacter[i] = false;
|
safeCharacter[i] = false;
|
||||||
}
|
}
|
||||||
const char *unsafeCharacters = ":/+?=#%@ ";
|
const char *unsafeCharacters = ":/+?=#%@ ";
|
||||||
for (const char*p = unsafeCharacters; *p != 0; ++p)
|
for (const char *p = unsafeCharacters; *p != 0; ++p)
|
||||||
{
|
{
|
||||||
safeCharacter[(uint8_t)*p] = false;
|
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;
|
} 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;
|
std::stringstream s;
|
||||||
encode_url_segment(s,pStart,pEnd,isQuerySegment);
|
encode_url_segment(s, pStart, pEnd, isQuerySegment);
|
||||||
return s.str();
|
return s.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
static char hexChars[] = "0123456789ABCDEF";
|
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)
|
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))
|
if (g_safeCharacterMap.isSafeCharacter(c))
|
||||||
{
|
{
|
||||||
os << c;
|
os << c;
|
||||||
} else if (c == ' ' && isQuerySegment)
|
}
|
||||||
|
else if (c == ' ' && isQuerySegment)
|
||||||
{
|
{
|
||||||
os << '+';
|
os << '+';
|
||||||
|
|
||||||
os << c;
|
os << c;
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
os << '%' << hexChars[(c >> 4) & 0x0f] << hexChars[c & 0x0F];
|
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;
|
std::stringstream s;
|
||||||
auto p = pStart;
|
auto p = pStart;
|
||||||
@@ -155,16 +156,23 @@ std::string HtmlHelper::decode_url_segment(const char*pStart, const char*pEnd, b
|
|||||||
if (c == '+')
|
if (c == '+')
|
||||||
{
|
{
|
||||||
s << ' ';
|
s << ' ';
|
||||||
} else if (c != '%')
|
}
|
||||||
|
else if (c != '%')
|
||||||
{
|
{
|
||||||
s << 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.");
|
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();
|
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)
|
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)
|
if (uc < 0x80u)
|
||||||
{
|
{
|
||||||
s << (char)uc;
|
s << (char)uc;
|
||||||
} else if (uc < 0x800u) {
|
}
|
||||||
|
else if (uc < 0x800u)
|
||||||
|
{
|
||||||
s << (char)(0xC0 + (uc >> 6));
|
s << (char)(0xC0 + (uc >> 6));
|
||||||
s << (char)(0x80 + (uc & 0x3F));
|
s << (char)(0x80 + (uc & 0x3F));
|
||||||
|
}
|
||||||
} else if (uc < 0x10000u) {
|
else if (uc < 0x10000u)
|
||||||
|
{
|
||||||
s << (char)(0xE0 + (uc >> 12));
|
s << (char)(0xE0 + (uc >> 12));
|
||||||
s << (char)(0x80 + ((uc >> 6) & 0x3F));
|
s << (char)(0x80 + ((uc >> 6) & 0x3F));
|
||||||
s << (char)(0x80 + (uc & 0x3F));
|
s << (char)(0x80 + (uc & 0x3F));
|
||||||
} else if (uc < 0x0110000) {
|
}
|
||||||
|
else if (uc < 0x0110000)
|
||||||
|
{
|
||||||
s << (char)(0xF0 + (uc >> 18));
|
s << (char)(0xF0 + (uc >> 18));
|
||||||
s << (char)(0x80 + ((uc >> 12) & 0x3F));
|
s << (char)(0x80 + ((uc >> 12) & 0x3F));
|
||||||
s << (char)(0x80 + ((uc >> 6) & 0x3F));
|
s << (char)(0x80 + ((uc >> 6) & 0x3F));
|
||||||
s << (char)(0x80 + (uc & 0x3F));
|
s << (char)(0x80 + (uc & 0x3F));
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
throw std::range_error("Illegal UTF-32 character.");
|
throw std::range_error("Illegal UTF-32 character.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,20 +220,22 @@ const std::string ESPECIALS = "()<>@,;:\"/[]?.=";
|
|||||||
#define MAX_FILENAME_LENGTH 96
|
#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.
|
#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.
|
// Encode all characters. Let the browser handle safe-ifying the actual name, since that's os dependent.
|
||||||
std::stringstream s;
|
std::stringstream s;
|
||||||
std::string prefix = "UTF-8''";
|
std::string prefix = "UTF-8''";
|
||||||
std::string postfix = "";
|
std::string postfix = "";
|
||||||
s << prefix;
|
s << prefix;
|
||||||
for (char c: name)
|
for (char c : name)
|
||||||
{
|
{
|
||||||
uint8_t uc = (uint8_t)c;
|
uint8_t uc = (uint8_t)c;
|
||||||
if (uc < 0x20 || uc >= 0x80 || ESPECIALS.find(c) != std::string::npos)
|
if (uc < 0x20 || uc >= 0x80 || ESPECIALS.find(c) != std::string::npos)
|
||||||
{
|
{
|
||||||
s << '%' << hexChars[uc >> 4] << hexChars[uc & 0x0F];
|
s << '%' << hexChars[uc >> 4] << hexChars[uc & 0x0F];
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
s << c;
|
s << c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,7 +243,6 @@ std::string HtmlHelper::Rfc5987EncodeFileName(const std::string&name)
|
|||||||
return s.str();
|
return s.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#define MAX_FILE_NAME_LENGHT 96
|
#define MAX_FILE_NAME_LENGHT 96
|
||||||
|
|
||||||
const std::string SF_SPECIALS = " <>@;:\"\'/[]?=";
|
const std::string SF_SPECIALS = " <>@;:\"\'/[]?=";
|
||||||
@@ -249,23 +265,126 @@ std::string HtmlHelper::SafeFileName(const std::string &name)
|
|||||||
return s.str();
|
return s.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string HtmlHelper::HtmlEncode(const std::string& text)
|
std::string HtmlHelper::HtmlEncode(const std::string &text)
|
||||||
{
|
{
|
||||||
std::stringstream os;
|
std::stringstream os;
|
||||||
for (char c: text)
|
for (char c : text)
|
||||||
{
|
{
|
||||||
switch(c)
|
switch (c)
|
||||||
{
|
{
|
||||||
case '<': os << "<"; break;
|
case '<':
|
||||||
case '>': os << ">"; break;
|
os << "<";
|
||||||
case '&': os << "&"; break;
|
break;
|
||||||
default: os << c; break;
|
case '>':
|
||||||
|
os << ">";
|
||||||
|
break;
|
||||||
|
case '&':
|
||||||
|
os << "&";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
os << c;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return os.str();
|
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 {
|
class HtmlHelper {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
static std::string timeToHttpDate();
|
static std::string timeToHttpDate();
|
||||||
static std::string timeToHttpDate(time_t time);
|
static std::string timeToHttpDate(time_t time);
|
||||||
static std::string timeToHttpDate(std::filesystem::file_time_type 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 SafeFileName(const std::string &name);
|
||||||
static std::string HtmlEncode(const std::string& text);
|
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.
|
} // namespace.
|
||||||
@@ -218,6 +218,10 @@ bool pipedal::IsSubdirectory(const std::filesystem::path &path, const std::files
|
|||||||
|
|
||||||
if ((*i) != (*iPath))
|
if ((*i) != (*iPath))
|
||||||
{
|
{
|
||||||
|
if (*i == "" && ++i == basePath.end()) // match xyz/ (trailing /)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
++iPath;
|
++iPath;
|
||||||
|
|||||||
+41
-16
@@ -100,7 +100,7 @@ Lv2Effect::Lv2Effect(
|
|||||||
lilv_node_free(uriNode);
|
lilv_node_free(uriNode);
|
||||||
{
|
{
|
||||||
AutoLilvNode bundleUri = lilv_plugin_get_bundle_uri(pPlugin);
|
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();
|
std::string storagePath = pHost_->GetPluginStoragePath();
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ Lv2Effect::Lv2Effect(
|
|||||||
pHost_->GetMapFeature().GetMap(),
|
pHost_->GetMapFeature().GetMap(),
|
||||||
logFeature.GetLog(),
|
logFeature.GetLog(),
|
||||||
bundleUriString,
|
bundleUriString,
|
||||||
storagePath);
|
storagePath);
|
||||||
|
|
||||||
mapPathFeature.Prepare(&(pHost_->GetMapFeature()));
|
mapPathFeature.Prepare(&(pHost_->GetMapFeature()));
|
||||||
mapPathFeature.SetPluginStoragePath(pHost_->GetPluginStoragePath());
|
mapPathFeature.SetPluginStoragePath(pHost_->GetPluginStoragePath());
|
||||||
@@ -117,11 +117,12 @@ Lv2Effect::Lv2Effect(
|
|||||||
const auto &fileProperties = info_->piPedalUI()->fileProperties();
|
const auto &fileProperties = info_->piPedalUI()->fileProperties();
|
||||||
for (const auto &fileProperty : fileProperties)
|
for (const auto &fileProperty : fileProperties)
|
||||||
{
|
{
|
||||||
if (!fileProperty->resourceDirectory().empty())
|
fs::path targetPath = fileProperty->directory() / std::filesystem::path(bundleUriString).parent_path().filename();
|
||||||
{
|
mapPathFeature.AddResourceFileMapping({
|
||||||
mapPathFeature.AddResourceFileMapping({makeAbsolutePath(fileProperty->resourceDirectory(), bundleUriString),
|
bundleUriString,
|
||||||
makeAbsolutePath(fileProperty->directory(), pHost_->GetPluginStoragePath())});
|
storagePath / targetPath,
|
||||||
}
|
fileProperty->fileTypes()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,14 +244,41 @@ Lv2Effect::Lv2Effect(
|
|||||||
// now that we've loaded the preset, clear the uri, and save new state
|
// now that we've loaded the preset, clear the uri, and save new state
|
||||||
// Why? because lilv doesn't provide facilities for reading state.
|
// Why? because lilv doesn't provide facilities for reading state.
|
||||||
pedalboardItem.lv2State(this->stateInterface->Save());
|
pedalboardItem.lv2State(this->stateInterface->Save());
|
||||||
|
RestoreState(pedalboardItem); // reload it with OUR map/unmap handling.
|
||||||
pedalboardItem.lilvPresetUri("");
|
pedalboardItem.lilvPresetUri("");
|
||||||
}
|
}
|
||||||
else
|
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.
|
// Restore state if present.
|
||||||
if (this->stateInterface)
|
if (this->stateInterface)
|
||||||
@@ -260,20 +288,18 @@ void Lv2Effect::RestoreState(PedalboardItem &pedalboardItem)
|
|||||||
if (pedalboardItem.lv2State().isValid_)
|
if (pedalboardItem.lv2State().isValid_)
|
||||||
{
|
{
|
||||||
this->stateInterface->Restore(pedalboardItem.lv2State());
|
this->stateInterface->Restore(pedalboardItem.lv2State());
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
else
|
return false;
|
||||||
{
|
|
||||||
// set the state to default state.
|
|
||||||
auto savedState = this->stateInterface->Save();
|
|
||||||
pedalboardItem.lv2State(savedState);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (const std::exception &e)
|
catch (const std::exception &e)
|
||||||
{
|
{
|
||||||
std::string name = pedalboardItem.pluginName();
|
std::string name = pedalboardItem.pluginName();
|
||||||
Lv2Log::warning(SS(name << ": " << e.what()));
|
Lv2Log::warning(SS(name << ": " << e.what()));
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Lv2Effect::ConnectControlPorts()
|
void Lv2Effect::ConnectControlPorts()
|
||||||
@@ -1116,4 +1142,3 @@ void Lv2Effect::SetPathPatchProperty(const std::string &propertyUri, const std::
|
|||||||
{
|
{
|
||||||
mainThreadPathProperties[propertyUri] = jsonAtom;
|
mainThreadPathProperties[propertyUri] = jsonAtom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -61,7 +61,7 @@ namespace pipedal
|
|||||||
|
|
||||||
FileBrowserFilesFeature fileBrowserFilesFeature;
|
FileBrowserFilesFeature fileBrowserFilesFeature;
|
||||||
std::unique_ptr<StateInterface> stateInterface;
|
std::unique_ptr<StateInterface> stateInterface;
|
||||||
void RestoreState(PedalboardItem&pedalboardItem);
|
bool RestoreState(PedalboardItem&pedalboardItem);
|
||||||
LogFeature logFeature;
|
LogFeature logFeature;
|
||||||
std::map<std::string,AtomBuffer> patchPropertyPrototypes;
|
std::map<std::string,AtomBuffer> patchPropertyPrototypes;
|
||||||
|
|
||||||
|
|||||||
@@ -65,5 +65,8 @@ namespace pipedal {
|
|||||||
const LV2_URID_Map *GetMap() const { return ↦}
|
const LV2_URID_Map *GetMap() const { return ↦}
|
||||||
LV2_URID_Map *GetMap() { 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 <algorithm>
|
||||||
#include "json.hpp"
|
#include "json.hpp"
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include "PluginHost.hpp"
|
||||||
|
#include "util.hpp"
|
||||||
|
|
||||||
|
|
||||||
using namespace pipedal;
|
using namespace pipedal;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
MapPathFeature::MapPathFeature()
|
MapPathFeature::MapPathFeature()
|
||||||
{
|
{
|
||||||
@@ -69,6 +72,27 @@ void MapPathFeature::FnFreePath(LV2_State_Free_Path_Handle handle, char* path)
|
|||||||
return ((MapPathFeature *)handle)->AbsolutePath(abstract_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)
|
char *MapPathFeature::AbsolutePath(const char *abstract_path)
|
||||||
{
|
{
|
||||||
if (strlen(abstract_path) == 0)
|
if (strlen(abstract_path) == 0)
|
||||||
@@ -76,7 +100,27 @@ char *MapPathFeature::AbsolutePath(const char *abstract_path)
|
|||||||
return strdup("");
|
return strdup("");
|
||||||
}
|
}
|
||||||
std::filesystem::path t (abstract_path);
|
std::filesystem::path t (abstract_path);
|
||||||
|
std::string extension = t.extension();
|
||||||
if (t.is_absolute()) {
|
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);
|
return strdup(abstract_path);
|
||||||
}
|
}
|
||||||
std::filesystem::path result = storagePath / t;
|
std::filesystem::path result = storagePath / t;
|
||||||
|
|||||||
+11
-2
@@ -21,17 +21,24 @@
|
|||||||
#include "lv2/state/state.h"
|
#include "lv2/state/state.h"
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include "MapFeature.hpp"
|
#include "MapFeature.hpp"
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
#include <filesystem>
|
||||||
|
#include "PiPedalUI.hpp"
|
||||||
|
|
||||||
namespace pipedal
|
namespace pipedal
|
||||||
{
|
{
|
||||||
|
class Lv2PluginInfo;
|
||||||
struct ResourceFileMapping {
|
struct ResourceFileMapping {
|
||||||
std::string resourcePath; // absolute path of the resource directory.
|
std::filesystem::path resourcePath; // absolute path of the resource directory.
|
||||||
std::string storagePath; // absolute path of where resource directory files get placed.
|
std::filesystem::path storagePath; // absolute path of where resource directory files get placed.
|
||||||
|
std::vector<UiFileType> fileTypes;
|
||||||
};
|
};
|
||||||
class MapPathFeature
|
class MapPathFeature
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
MapPathFeature();
|
MapPathFeature();
|
||||||
|
|
||||||
void Prepare(MapFeature* map);
|
void Prepare(MapFeature* map);
|
||||||
void SetPluginStoragePath(const std::string&path) { storagePath = path;}
|
void SetPluginStoragePath(const std::string&path) { storagePath = path;}
|
||||||
|
|
||||||
@@ -46,6 +53,8 @@ namespace pipedal
|
|||||||
const LV2_Feature*GetMakePathFeature() { return &makePathFeature;}
|
const LV2_Feature*GetMakePathFeature() { return &makePathFeature;}
|
||||||
const LV2_Feature*GetFreePathFeature() { return &freePathFeature;}
|
const LV2_Feature*GetFreePathFeature() { return &freePathFeature;}
|
||||||
private:
|
private:
|
||||||
|
|
||||||
|
std::shared_ptr<Lv2PluginInfo> pluginInfo;
|
||||||
char *AbsolutePath(const char *abstract_path);
|
char *AbsolutePath(const char *abstract_path);
|
||||||
static char *FnAbsolutePath(LV2_State_Map_Path_Handle handle,
|
static char *FnAbsolutePath(LV2_State_Map_Path_Handle handle,
|
||||||
const char *abstract_path);
|
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::vector<std::string> fileTypesX; // mixture of .ext and mime types. Use fileExtensions instead.
|
||||||
const std::set<std::string> fileExtensions;
|
const std::set<std::string> fileExtensions;
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static const std::vector<ModDirectory> &ModDirectories();
|
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.
|
// Feed them no files; they will be added later.
|
||||||
json_variant filesObj = json_variant::make_array();
|
json_variant filesObj = json_variant::make_array();
|
||||||
json_variant fileObj = json_variant::make_object();
|
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["fullname"] = "{{fullname}}";
|
||||||
fileObj["basename"] = "{{basename}}";
|
fileObj["basename"] = "{{basename}}";
|
||||||
filesObj.as_array()->push_back(fileObj);
|
filesObj.as_array()->push_back(fileObj);
|
||||||
|
|||||||
@@ -115,6 +115,18 @@ const ControlValue* PedalboardItem::GetControlValue(const std::string&symbol) co
|
|||||||
return nullptr;
|
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)
|
bool Pedalboard::SetItemEnabled(int64_t pedalItemId, bool enabled)
|
||||||
{
|
{
|
||||||
PedalboardItem*item = GetItem(pedalItemId);
|
PedalboardItem*item = GetItem(pedalItemId);
|
||||||
@@ -513,6 +525,7 @@ JSON_MAP_BEGIN(PedalboardItem)
|
|||||||
JSON_MAP_REFERENCE(PedalboardItem,lilvPresetUri)
|
JSON_MAP_REFERENCE(PedalboardItem,lilvPresetUri)
|
||||||
JSON_MAP_REFERENCE(PedalboardItem,pathProperties)
|
JSON_MAP_REFERENCE(PedalboardItem,pathProperties)
|
||||||
JSON_MAP_REFERENCE(PedalboardItem,title)
|
JSON_MAP_REFERENCE(PedalboardItem,title)
|
||||||
|
JSON_MAP_REFERENCE(PedalboardItem,useModUi)
|
||||||
JSON_MAP_END()
|
JSON_MAP_END()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ public:
|
|||||||
std::string lilvPresetUri_;
|
std::string lilvPresetUri_;
|
||||||
std::map<std::string,std::string> pathProperties_;
|
std::map<std::string,std::string> pathProperties_;
|
||||||
std::string title_;
|
std::string title_;
|
||||||
|
bool useModUi_ = false;
|
||||||
|
|
||||||
// non persistent state.
|
// non persistent state.
|
||||||
PropertyMap patchProperties;
|
PropertyMap patchProperties;
|
||||||
@@ -128,6 +129,7 @@ public:
|
|||||||
GETTER_SETTER(stateUpdateCount)
|
GETTER_SETTER(stateUpdateCount)
|
||||||
GETTER_SETTER_REF(lv2State)
|
GETTER_SETTER_REF(lv2State)
|
||||||
GETTER_SETTER_REF(title)
|
GETTER_SETTER_REF(title)
|
||||||
|
GETTER_SETTER(useModUi)
|
||||||
|
|
||||||
Lv2PluginState&lv2State() { return lv2State_; } // non-const version.
|
Lv2PluginState&lv2State() { return lv2State_; } // non-const version.
|
||||||
GETTER_SETTER_REF(lilvPresetUri)
|
GETTER_SETTER_REF(lilvPresetUri)
|
||||||
@@ -203,6 +205,7 @@ public:
|
|||||||
bool SetControlValue(int64_t pedalItemId, const std::string &symbol, float value);
|
bool SetControlValue(int64_t pedalItemId, const std::string &symbol, float value);
|
||||||
bool SetItemTitle(int64_t pedalItemId, const std::string &title);
|
bool SetItemTitle(int64_t pedalItemId, const std::string &title);
|
||||||
bool SetItemEnabled(int64_t pedalItemId, bool enabled);
|
bool SetItemEnabled(int64_t pedalItemId, bool enabled);
|
||||||
|
bool SetItemUseModUi(int64_t pedalItemId, bool enabled);
|
||||||
void SetCurrentSnapshotModified(bool modified);
|
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.
|
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)
|
void PiPedalModel::SetPedalboardItemEnable(int64_t clientId, int64_t pedalItemId, bool enabled)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::recursive_mutex> guard{mutex};
|
std::lock_guard<std::recursive_mutex> guard{mutex};
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ namespace pipedal
|
|||||||
|
|
||||||
virtual int64_t GetClientId() = 0;
|
virtual int64_t GetClientId() = 0;
|
||||||
virtual void OnItemEnabledChanged(int64_t clientId, int64_t pedalItemId, bool enabled) = 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 OnControlChanged(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value) = 0;
|
||||||
virtual void OnInputVolumeChanged(float value) = 0;
|
virtual void OnInputVolumeChanged(float value) = 0;
|
||||||
virtual void OnOutputVolumeChanged(float value) = 0;
|
virtual void OnOutputVolumeChanged(float value) = 0;
|
||||||
@@ -348,6 +349,7 @@ namespace pipedal
|
|||||||
void RemoveNotificationSubsription(std::shared_ptr<IPiPedalModelSubscriber> pSubscriber);
|
void RemoveNotificationSubsription(std::shared_ptr<IPiPedalModelSubscriber> pSubscriber);
|
||||||
|
|
||||||
void SetPedalboardItemEnable(int64_t clientId, int64_t instanceId, bool enabled);
|
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 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);
|
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_REFERENCE(PedalboardItemEnabledBody, enabled)
|
||||||
JSON_MAP_END()
|
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
|
class UpdateCurrentPedalboardBody
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -1303,6 +1319,11 @@ public:
|
|||||||
pReader->read(&body);
|
pReader->read(&body);
|
||||||
model.SetPedalboardItemEnable(body.clientId_, body.instanceId_, body.enabled_);
|
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")
|
else if (message == "updateCurrentPedalboard")
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
@@ -2221,6 +2242,15 @@ private:
|
|||||||
body.enabled_ = enabled;
|
body.enabled_ = enabled;
|
||||||
Send("onItemEnabledChanged", body);
|
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;
|
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)
|
UiFileProperty::UiFileProperty(PluginHost *pHost, const LilvNode *node, const std::filesystem::path &resourcePath)
|
||||||
{
|
{
|
||||||
auto pWorld = pHost->getWorld();
|
auto pWorld = pHost->getWorld();
|
||||||
@@ -455,6 +456,13 @@ bool UiFileType::IsValidExtension(const std::string&extension) const {
|
|||||||
return false;
|
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)
|
UiFileType::UiFileType(const std::string&label, const std::string &fileType)
|
||||||
: label_(label)
|
: label_(label)
|
||||||
, fileExtension_(fileType)
|
, fileExtension_(fileType)
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ namespace pipedal
|
|||||||
{
|
{
|
||||||
|
|
||||||
class PluginHost;
|
class PluginHost;
|
||||||
|
class ModFileTypes;
|
||||||
|
|
||||||
class UiFileType
|
class UiFileType
|
||||||
{
|
{
|
||||||
@@ -87,12 +88,16 @@ namespace pipedal
|
|||||||
UiFileType() {}
|
UiFileType() {}
|
||||||
UiFileType(PluginHost *pHost, const LilvNode *node);
|
UiFileType(PluginHost *pHost, const LilvNode *node);
|
||||||
UiFileType(const std::string &label, const std::string &fileType);
|
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);
|
static std::vector<UiFileType> GetArray(PluginHost *pHost, const LilvNode *node, const LilvNode *uri);
|
||||||
|
|
||||||
const std::string &label() const { return label_; }
|
const std::string &label() const { return label_; }
|
||||||
|
void label(const std::string &value) { label_ = value; }
|
||||||
const std::string &fileExtension() const { return fileExtension_; }
|
const std::string &fileExtension() const { return fileExtension_; }
|
||||||
|
void fileExtension(const std::string &value) { fileExtension_ = value; }
|
||||||
const std::string &mimeType() const { return mimeType_; }
|
const std::string &mimeType() const { return mimeType_; }
|
||||||
|
void mimeType(const std::string &value) { mimeType_ = value; }
|
||||||
bool IsValidExtension(const std::string &extension) const;
|
bool IsValidExtension(const std::string &extension) const;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -160,6 +165,8 @@ namespace pipedal
|
|||||||
|
|
||||||
const std::vector<UiFileType> &fileTypes() const { return fileTypes_; }
|
const std::vector<UiFileType> &fileTypes() const { return fileTypes_; }
|
||||||
std::vector<UiFileType> &fileTypes() { 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_; }
|
const std::string &patchProperty() const { return patchProperty_; }
|
||||||
|
|
||||||
|
|||||||
+83
-11
@@ -647,6 +647,37 @@ bool Lv2PluginInfo::HasFactoryPresets(PluginHost *lv2Host, const LilvPlugin *plu
|
|||||||
return result;
|
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::FindWritablePathPropertiesResult
|
||||||
Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin *pPlugin)
|
Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin *pPlugin)
|
||||||
{
|
{
|
||||||
@@ -710,6 +741,13 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
|
|||||||
// "nam,nammodel"
|
// "nam,nammodel"
|
||||||
fileTypes = mod__fileTypes.AsString();
|
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);
|
ModFileTypes modFileTypes(fileTypes);
|
||||||
|
|
||||||
// Legacy case: audio_uploads/<plugin_directory> exists.
|
// Legacy case: audio_uploads/<plugin_directory> exists.
|
||||||
@@ -734,6 +772,12 @@ Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin
|
|||||||
std::string modName = modFileTypes.rootDirectories()[0];
|
std::string modName = modFileTypes.rootDirectories()[0];
|
||||||
auto modDirectory = modFileTypes.GetModDirectory(modName);
|
auto modDirectory = modFileTypes.GetModDirectory(modName);
|
||||||
fileProperty->directory(modDirectory->pipedalPath);
|
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)
|
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.
|
// properies with indexes first.
|
||||||
int32_t indexL = left->index();
|
int32_t indexL = left->index();
|
||||||
if (indexL < 0)
|
if (indexL < 0)
|
||||||
@@ -888,7 +935,7 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP
|
|||||||
// a lv2:Parameter?
|
// a lv2:Parameter?
|
||||||
if (lilv_world_ask(pWorld, propertyUri, lv2Host->lilvUris->isA, lv2Host->lilvUris->lv2core__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);
|
propertyInfo.writable(true);
|
||||||
patchProperties_.push_back(std::move(propertyInfo));
|
patchProperties_.push_back(std::move(propertyInfo));
|
||||||
}
|
}
|
||||||
@@ -915,18 +962,33 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!found)
|
if (!found)
|
||||||
{
|
{
|
||||||
if (lilv_world_ask(pWorld, propertyUri, lv2Host->lilvUris->isA, lv2Host->lilvUris->lv2core__Parameter))
|
if (lilv_world_ask(pWorld, propertyUri, lv2Host->lilvUris->isA, lv2Host->lilvUris->lv2core__Parameter))
|
||||||
{
|
{
|
||||||
Lv2PatchPropertyInfo propertyInfo{lv2Host, propertyUri};
|
Lv2PatchPropertyInfo propertyInfo{lv2Host, propertyUri};
|
||||||
propertyInfo.readable(true);
|
propertyInfo.readable(true);
|
||||||
patchProperties_.push_back(std::move(propertyInfo));
|
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
|
// Fetch pipedal plugin UI
|
||||||
@@ -976,6 +1038,10 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP
|
|||||||
++nOutputs;
|
++nOutputs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (nOutputs == 0 || nInputs == 0)
|
||||||
|
{
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
this->modGui_ = ModGui::Create(lv2Host, pPlugin);
|
this->modGui_ = ModGui::Create(lv2Host, pPlugin);
|
||||||
this->is_valid_ = isValid;
|
this->is_valid_ = isValid;
|
||||||
}
|
}
|
||||||
@@ -1776,7 +1842,7 @@ Lv2PatchPropertyInfo::Lv2PatchPropertyInfo(PluginHost *pluginHost, const LilvNod
|
|||||||
{
|
{
|
||||||
LilvWorld *pWorld = pluginHost->getWorld();
|
LilvWorld *pWorld = pluginHost->getWorld();
|
||||||
this->uri_ = nodeAsString(propertyUri);
|
this->uri_ = nodeAsString(propertyUri);
|
||||||
|
|
||||||
AutoLilvNode range = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->rdfs__range, nullptr);
|
AutoLilvNode range = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->rdfs__range, nullptr);
|
||||||
if (range)
|
if (range)
|
||||||
{
|
{
|
||||||
@@ -1791,7 +1857,9 @@ Lv2PatchPropertyInfo::Lv2PatchPropertyInfo(PluginHost *pluginHost, const LilvNod
|
|||||||
if (shortName)
|
if (shortName)
|
||||||
{
|
{
|
||||||
this->shortName_ = shortName.AsString();
|
this->shortName_ = shortName.AsString();
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
this->shortName_ = this->label_;
|
this->shortName_ = this->label_;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1799,14 +1867,16 @@ Lv2PatchPropertyInfo::Lv2PatchPropertyInfo(PluginHost *pluginHost, const LilvNod
|
|||||||
this->index_ = indexNode.AsInt(-1);
|
this->index_ = indexNode.AsInt(-1);
|
||||||
|
|
||||||
AutoLilvNode modFileTypesNode = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->mod__fileTypes, nullptr);
|
AutoLilvNode modFileTypesNode = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->mod__fileTypes, nullptr);
|
||||||
if (modFileTypesNode) {
|
if (modFileTypesNode)
|
||||||
|
{
|
||||||
std::string strFileTypes = modFileTypesNode.AsString();
|
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);
|
AutoLilvNode modSupportedExtensionsNode = lilv_world_get(pWorld, propertyUri, pluginHost->lilvUris->mod__supportedExtensions, nullptr);
|
||||||
if (modSupportedExtensionsNode) {
|
if (modSupportedExtensionsNode)
|
||||||
|
{
|
||||||
std::string strSupportedExtensions = modSupportedExtensionsNode.AsString();
|
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("has_factory_presets", &Lv2PluginInfo::has_factory_presets_),
|
||||||
json_map::reference("modGui", &Lv2PluginInfo::modGui_),
|
json_map::reference("modGui", &Lv2PluginInfo::modGui_),
|
||||||
json_map::reference("patchProperties", &Lv2PluginInfo::patchProperties_),
|
json_map::reference("patchProperties", &Lv2PluginInfo::patchProperties_),
|
||||||
|
json_map::reference("hasDefaultState", &Lv2PluginInfo::hasDefaultState_),
|
||||||
|
|
||||||
}};
|
}};
|
||||||
|
|
||||||
json_map::storage_type<Lv2PluginClass> Lv2PluginClass::jmap{{
|
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
|
// 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
|
// 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<Lv2PortInfo>> ports_;
|
||||||
std::vector<std::shared_ptr<Lv2PortGroup>> port_groups_;
|
std::vector<std::shared_ptr<Lv2PortGroup>> port_groups_;
|
||||||
std::vector<Lv2PatchPropertyInfo> patchProperties_;
|
std::vector<Lv2PatchPropertyInfo> patchProperties_;
|
||||||
|
|
||||||
|
bool hasDefaultState_;
|
||||||
|
|
||||||
bool is_valid_ = false;
|
bool is_valid_ = false;
|
||||||
PiPedalUI::ptr piPedalUI_;
|
PiPedalUI::ptr piPedalUI_;
|
||||||
@@ -450,6 +452,7 @@ namespace pipedal
|
|||||||
LV2_PROPERTY_GETSET(hasUnsupportedPatchProperties)
|
LV2_PROPERTY_GETSET(hasUnsupportedPatchProperties)
|
||||||
LV2_PROPERTY_GETSET(modGui)
|
LV2_PROPERTY_GETSET(modGui)
|
||||||
LV2_PROPERTY_GETSET(patchProperties)
|
LV2_PROPERTY_GETSET(patchProperties)
|
||||||
|
LV2_PROPERTY_GETSET(hasDefaultState)
|
||||||
|
|
||||||
const Lv2PortInfo &getPort(const std::string &symbol)
|
const Lv2PortInfo &getPort(const std::string &symbol)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#include "json.hpp"
|
#include "json.hpp"
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include "Lv2Log.hpp"
|
||||||
|
|
||||||
using namespace pipedal;
|
using namespace pipedal;
|
||||||
|
|
||||||
@@ -83,6 +84,7 @@ static void CheckState(LV2_State_Status status)
|
|||||||
case LV2_State_Status::LV2_STATE_SUCCESS:
|
case LV2_State_Status::LV2_STATE_SUCCESS:
|
||||||
return;
|
return;
|
||||||
default:
|
default:
|
||||||
|
case LV2_State_Status::LV2_STATE_ERR_UNKNOWN:
|
||||||
lv2Error = "Unknown error.";
|
lv2Error = "Unknown error.";
|
||||||
break;
|
break;
|
||||||
case LV2_State_Status::LV2_STATE_ERR_BAD_TYPE:
|
case LV2_State_Status::LV2_STATE_ERR_BAD_TYPE:
|
||||||
@@ -126,7 +128,8 @@ Lv2PluginState StateInterface::Save()
|
|||||||
}
|
}
|
||||||
catch (const std::exception &e)
|
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);
|
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
|
res.set("Cache-Control", "public, max-age=31536000"); // 1 month
|
||||||
auto lastModified = std::filesystem::last_write_time(path);
|
auto lastModified = std::filesystem::last_write_time(path);
|
||||||
res.set(HttpField::LastModified, HtmlHelper::timeToHttpDate(lastModified));
|
res.set(HttpField::LastModified, HtmlHelper::timeToHttpDate(lastModified));
|
||||||
|
// Set ETag for cache validation
|
||||||
|
res.set("ETag", pipedal::HtmlHelper::generateEtag(path));
|
||||||
}
|
}
|
||||||
else
|
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.
|
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
|
Remove FullScreenIME in PluginControlView.tsx
|
||||||
|
|
||||||
Exactly one underrun per seek in Toob Player
|
Exactly one underrun per seek in Toob Player
|
||||||
|
|
||||||
Test autohotspot detection.
|
Test autohotspot detection.
|
||||||
|
|
||||||
|
Ellipsis on plugin Title
|
||||||
|
|
||||||
check filetime conversion in gcc 12.2! (missing c++ 20 time conversion functions)
|
check filetime conversion in gcc 12.2! (missing c++ 20 time conversion functions)
|
||||||
AudioFiles.cpp fileTimeToInt64
|
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 {
|
.mod-pedal {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +81,11 @@
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mod-pedal-settings .mod-control-group {
|
||||||
|
margin:0;
|
||||||
|
padding:0 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.ppmod-dial-control-image {
|
.ppmod-dial-control-image {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
@@ -61,4 +107,71 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
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 ?
|
{(!this.state.tinyToolBar) && !this.state.performanceView ?
|
||||||
(
|
(
|
||||||
<AppBar position="absolute" >
|
<AppBar position="absolute" >
|
||||||
<Toolbar variant="dense" className={classes.toolBar} >
|
<Toolbar variant="dense" className={classes.toolBar} style={{overflow: "clip" }} >
|
||||||
<IconButtonEx tooltip="Menu"
|
<IconButtonEx tooltip="Menu"
|
||||||
edge="start"
|
edge="start"
|
||||||
aria-label="menu"
|
aria-label="menu"
|
||||||
|
|||||||
+368
-21
@@ -1,23 +1,370 @@
|
|||||||
/*
|
// Copyright (c) 2025 Robin E. R. Davies
|
||||||
* 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, 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
|
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
import { Theme } from '@mui/material/styles';
|
||||||
* in the Software without restriction, including without limitation the rights
|
import WithStyles from './WithStyles';
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
import { withStyles } from "tss-react/mui";
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
import { createStyles } from './WithStyles';
|
||||||
* furnished to do so, subject to the following conditions:
|
import FullscreenIcon from '@mui/icons-material/Fullscreen';
|
||||||
|
import IconButtonEx from './IconButtonEx';
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
import { isDarkMode } from './DarkMode';
|
||||||
* copies or substantial portions of the Software.
|
import Backdrop from '@mui/material/Backdrop';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
import Rect from './Rect';
|
||||||
* 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 { 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: "Pirulen", cssRef: "/fonts/pirulen/stylesheet.css", fontWeights: [400] })}
|
||||||
{TypeSample({ fontFamily: "Questrial", cssRef: "/fonts/questrial/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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -110,6 +110,9 @@ const GxTunerView =
|
|||||||
item={this.props.item}
|
item={this.props.item}
|
||||||
customization={this}
|
customization={this}
|
||||||
customizationId={this.customizationId}
|
customizationId={this.customizationId}
|
||||||
|
showModGui={false}
|
||||||
|
onSetShowModGui= {(instanceId: number, showModGui: boolean) => {}}
|
||||||
|
|
||||||
/>);
|
/>);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+146
-50
@@ -20,118 +20,214 @@
|
|||||||
|
|
||||||
// Utility class for constructing Json atoms.
|
// Utility class for constructing Json atoms.
|
||||||
class JsonAtom {
|
class JsonAtom {
|
||||||
static Property(value: string): any
|
|
||||||
|
static _Bool(value: boolean): any
|
||||||
{
|
{
|
||||||
return {
|
return new JsonAtom({
|
||||||
"otype_": "Property",
|
|
||||||
"value": value
|
|
||||||
};
|
|
||||||
}
|
|
||||||
static Bool(value: boolean): any
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
"otype_": "Bool",
|
"otype_": "Bool",
|
||||||
"value": value
|
"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",
|
"otype_": "Int",
|
||||||
"value": value
|
"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",
|
"otype_": "Long",
|
||||||
"value": value
|
"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",
|
"otype_": "Double",
|
||||||
"value": value
|
"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",
|
"otype_": "Path",
|
||||||
"value": value
|
"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",
|
"otype_": "URI",
|
||||||
"value": value
|
"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",
|
"otype_": "URID",
|
||||||
"value": value
|
"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
|
"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",
|
"otype_": "Tuple",
|
||||||
"value": values
|
"value": values
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
static IntVector(values: []): any
|
static _IntVector(values: []): JsonAtom
|
||||||
{
|
{
|
||||||
return {
|
return new JsonAtom({
|
||||||
"otype_": "Vector",
|
"otype_": "Vector",
|
||||||
"vtype_": "Int",
|
"vtype_": "Int",
|
||||||
"value": values
|
"value": values
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
static LongVector(values: []): any
|
static _LongVector(values: []): JsonAtom
|
||||||
{
|
{
|
||||||
return {
|
return new JsonAtom({
|
||||||
"otype_": "Vector",
|
"otype_": "Vector",
|
||||||
"vtype_": "Long",
|
"vtype_": "Long",
|
||||||
"value": values
|
"value": values
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static FloatVector(values: []): any
|
static _FloatVector(values: []): JsonAtom
|
||||||
{
|
{
|
||||||
return {
|
return new JsonAtom({
|
||||||
"otype_": "Vector",
|
"otype_": "Vector",
|
||||||
"vtype_": "Float",
|
"vtype_": "Float",
|
||||||
"value": values
|
"value": values
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
static DoubleVector(values: []): any
|
static _DoubleVector(values: []): JsonAtom
|
||||||
{
|
{
|
||||||
return {
|
return new JsonAtom({
|
||||||
"otype_": "Vector",
|
"otype_": "Vector",
|
||||||
"vtype_": "Float",
|
"vtype_": "Float",
|
||||||
"value": values
|
"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;
|
export default JsonAtom;
|
||||||
|
|||||||
@@ -1021,6 +1021,17 @@ export class UiPlugin implements Deserializable<UiPlugin> {
|
|||||||
return null;
|
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 = "";
|
uri: string = "";
|
||||||
name: string = "";
|
name: string = "";
|
||||||
minorVersion: number = 0;
|
minorVersion: number = 0;
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ import PipedalUiIcon from './svg/pp_ui.svg?react';
|
|||||||
|
|
||||||
import SnapshotDialog from './SnapshotDialog';
|
import SnapshotDialog from './SnapshotDialog';
|
||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
|
import { setDefaultModGuiPreference } from './ModGuiHost';
|
||||||
|
|
||||||
|
|
||||||
const SPLIT_CONTROLBAR_THRESHHOLD = 750;
|
const SPLIT_CONTROLBAR_THRESHHOLD = 750;
|
||||||
@@ -253,6 +254,7 @@ export const MainPage =
|
|||||||
this.setState({
|
this.setState({
|
||||||
pedalboard: value,
|
pedalboard: value,
|
||||||
selectedPedal: selectedItem,
|
selectedPedal: selectedItem,
|
||||||
|
showModUi: value.maybeGetItem(selectedItem)?.useModUi ?? false,
|
||||||
selectedSnapshot: value.selectedSnapshot
|
selectedSnapshot: value.selectedSnapshot
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -279,6 +281,14 @@ export const MainPage =
|
|||||||
this.model.pedalboard.removeOnChangedHandler(this.onPedalboardChanged);
|
this.model.pedalboard.removeOnChangedHandler(this.onPedalboardChanged);
|
||||||
super.componentWillUnmount();
|
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() {
|
updateResponsive() {
|
||||||
this.setState({
|
this.setState({
|
||||||
splitControlBar: this.getSplitToolbar(),
|
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) {
|
onPedalboardPropertyChanged(instanceId: number, key: string, value: number) {
|
||||||
this.model.setPedalboardControl(instanceId, key, value);
|
this.model.setPedalboardControl(instanceId, key, value);
|
||||||
}
|
}
|
||||||
@@ -380,13 +399,14 @@ export const MainPage =
|
|||||||
if (pedalboardItem === null) return "";
|
if (pedalboardItem === null) return "";
|
||||||
return pedalboardItem.uri;
|
return pedalboardItem.uri;
|
||||||
}
|
}
|
||||||
titleBar(pedalboardItem: PedalboardItem | null,canShowModUi: boolean): React.ReactNode {
|
titleBar(pedalboardItem: PedalboardItem | null, canShowModUi: boolean): React.ReactNode {
|
||||||
let title = "";
|
let title = "";
|
||||||
let author = "";
|
let author = "";
|
||||||
let infoPluginUri = "";
|
let infoPluginUri = "";
|
||||||
let presetsUri = "";
|
let presetsUri = "";
|
||||||
let missing = false;
|
let missing = false;
|
||||||
let canEditTitle = false;
|
let canEditTitle = false;
|
||||||
|
let modGuiButtonVisible = false;
|
||||||
if (pedalboardItem) {
|
if (pedalboardItem) {
|
||||||
if (pedalboardItem.isEmpty()) {
|
if (pedalboardItem.isEmpty()) {
|
||||||
title = "";
|
title = "";
|
||||||
@@ -416,6 +436,7 @@ export const MainPage =
|
|||||||
// if (uiPlugin.description.length > 20) {
|
// if (uiPlugin.description.length > 20) {
|
||||||
// }
|
// }
|
||||||
infoPluginUri = uiPlugin.uri;
|
infoPluginUri = uiPlugin.uri;
|
||||||
|
modGuiButtonVisible = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -442,20 +463,25 @@ export const MainPage =
|
|||||||
display: "flex", flexDirection: "row", height: 48, flexWrap: "nowrap",
|
display: "flex", flexDirection: "row", height: 48, flexWrap: "nowrap",
|
||||||
alignItems: "center"
|
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 ? (
|
{canEditTitle ? (
|
||||||
<ButtonBase
|
<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={() => {
|
onClick={() => {
|
||||||
this.handleEditPluginDisplayName();
|
this.handleEditPluginDisplayName();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>
|
<div style={{ flex: "0 1 auto" }}>
|
||||||
<span className={classes.title}>{title}</span>
|
<span>
|
||||||
{this.state.displayAuthor && (
|
<span className={classes.title}>{title}</span>
|
||||||
<span className={classes.author}>{author}</span>
|
{this.state.displayAuthor && (
|
||||||
)}
|
<span className={classes.author}>{author}</span>
|
||||||
</span>
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</ButtonBase>)
|
</ButtonBase>)
|
||||||
: (
|
: (
|
||||||
<div style={{ flex: "0 1 auto", paddingLeft: 8, paddingRight: 8, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>
|
<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}
|
<PluginPresetSelector pluginUri={presetsUri} instanceId={pedalboardItem?.instanceId ?? 0}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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={() => {
|
onClick={() => {
|
||||||
this.setState({ showModUi: !this.state.showModUi });
|
this.handleShowModUi()
|
||||||
}}
|
}}
|
||||||
size="large">
|
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 }} />
|
<ModUiIcon style={{ height: 24, width: 24, fill: this.props.theme.palette.text.primary, opacity: 0.6 }} />
|
||||||
) : (
|
) : (
|
||||||
@@ -498,6 +530,7 @@ export const MainPage =
|
|||||||
}
|
}
|
||||||
</IconButtonEx>
|
</IconButtonEx>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</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) => {
|
(instanceId: number, showModGui: boolean) => {
|
||||||
|
this.model.setPedalboardItemUseModUi(instanceId, showModGui)
|
||||||
this.setState({ showModUi: showModGui });
|
this.setState({ showModUi: showModGui });
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
+448
-151
@@ -21,17 +21,21 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { UiPlugin, UiControl, ControlType } from './Lv2Plugin';
|
import { UiPlugin, UiControl, ControlType, UiFileProperty } from './Lv2Plugin';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
import IconButtonEx from './IconButtonEx';
|
import IconButtonEx from './IconButtonEx';
|
||||||
import Typography from '@mui/material/Typography/Typography';
|
import Typography from '@mui/material/Typography/Typography';
|
||||||
import ModGuiErrorBoundary from './ModGuiErrorBoundary';
|
import ModGuiErrorBoundary from './ModGuiErrorBoundary';
|
||||||
import OkDialog from './OkDialog';
|
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 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.
|
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 FINE_WHEEL_RANGE_SCALE = WHEEL_RANGE_SCALE * 10;
|
||||||
const ULTRA_FINE_WHEEL_RANGE_SCALE = WHEEL_RANGE_SCALE * 50;
|
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(
|
export type BypassChangedCallback = (value: boolean) => void;
|
||||||
instanceId: number,
|
|
||||||
propertyUri: string,
|
function HtmlEncode(value: string): string {
|
||||||
onReceived: PatchPropertyListener
|
value = value.replace(/&/g, '&')
|
||||||
): ListenHandle;
|
.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 {
|
export interface ModGuiHostProps {
|
||||||
instanceId: number,
|
instanceId: number,
|
||||||
@@ -69,7 +97,10 @@ export interface ModGuiHostProps {
|
|||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
test?: boolean;
|
test?: boolean;
|
||||||
hostSite: IModGuiHostSite;
|
handleFileSelect: (instanceId: number, propertyUri: string, filePath: string) => void;
|
||||||
|
onContentReady: (ready: boolean) => void;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
export interface ModGuiJs {
|
export interface ModGuiJs {
|
||||||
set_port_value: (symbol: string, value: number) => void;
|
set_port_value: (symbol: string, value: number) => void;
|
||||||
@@ -92,7 +123,10 @@ interface CustomSelectPathControlProps {
|
|||||||
instanceId: number;
|
instanceId: number;
|
||||||
plugin: UiPlugin;
|
plugin: UiPlugin;
|
||||||
propertyUri: string,
|
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;
|
this.props = props;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isInlineList: boolean = false;
|
||||||
|
|
||||||
requestUpdate() {
|
requestUpdate() {
|
||||||
if (!this.mounted) return;
|
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 pathValue: string | null = null;
|
||||||
private browsePath: string | null = null;
|
|
||||||
setPathValue(value: string) {
|
setPathValue(value: string) {
|
||||||
if (this.pathValue !== value) {
|
if (this.pathValue !== value) {
|
||||||
this.pathValue = value;
|
this.pathValue = value;
|
||||||
if (this.pathValue !== "") {
|
if (this.pathValue !== "" || this.navDirectory === null) {
|
||||||
let browsePath = pathParentDirectory(value);
|
let browsePath = pathParentDirectory(value);
|
||||||
if (this.browsePath !== browsePath) {
|
this.setNavDirectory(browsePath);
|
||||||
this.browsePath = browsePath;
|
|
||||||
this.requestFileUpdate();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
if (this.enumeratedValueElement) {
|
||||||
|
this.enumeratedValueElement.textContent = pathFileName(this.pathValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateImage() {
|
private updateImage() {
|
||||||
@@ -175,7 +336,8 @@ class CustomSelectPathControl implements ModGuiControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handlePropertyValueChange(value: any) {
|
handlePropertyValueChange(value: any) {
|
||||||
|
let path: string = PathToString(value);
|
||||||
|
this.setPathValue(path);
|
||||||
}
|
}
|
||||||
private mounted: boolean = false;
|
private mounted: boolean = false;
|
||||||
|
|
||||||
@@ -183,29 +345,29 @@ class CustomSelectPathControl implements ModGuiControl {
|
|||||||
onMounted() {
|
onMounted() {
|
||||||
this.mounted = true;
|
this.mounted = true;
|
||||||
this.props.hostSite.monitorPatchProperty(this.props.instanceId, this.props.propertyUri,
|
this.props.hostSite.monitorPatchProperty(this.props.instanceId, this.props.propertyUri,
|
||||||
(value: any) => {
|
(instanceId, propertyUri, value) => {
|
||||||
this.handlePropertyValueChange(value);
|
this.handlePropertyValueChange(value);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.props.hostSite.getPatchProperty(this.props.instanceId, this.props.propertyUri)
|
this.props.hostSite.getPatchProperty(this.props.instanceId, this.props.propertyUri)
|
||||||
.then((value: any) => {
|
.then((value: any) => {
|
||||||
this.handlePropertyValueChange(value);
|
this.handlePropertyValueChange(value);
|
||||||
}).
|
}).
|
||||||
catch((error: any) => {
|
catch((error: any) => {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
console.error((error as Error).message);
|
console.error((error as Error).message);
|
||||||
} else {
|
} else {
|
||||||
console.error(error.toString());
|
console.error(error.toString());
|
||||||
}
|
}
|
||||||
console.error(error);
|
this.handlePropertyValueChange("");
|
||||||
});
|
});
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
onUnmount() {
|
onUnmount() {
|
||||||
if (this.listenHandle) {
|
if (this.listenHandle) {
|
||||||
this.props.hostSite.cancelMonitorPatchProperty(this.listenHandle);
|
this.props.hostSite.cancelMonitorPatchProperty(this.listenHandle);
|
||||||
this.listenHandle = null;
|
this.listenHandle = null;
|
||||||
}
|
}
|
||||||
this.mounted = false;
|
this.mounted = false;
|
||||||
this.cancelRequestUpdate();
|
this.cancelRequestUpdate();
|
||||||
@@ -226,40 +388,92 @@ class CustomSelectPathControl implements ModGuiControl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSelected(value: string,strFileType: string)
|
|
||||||
{
|
getValueElement(): HTMLElement | null {
|
||||||
this.setPathValue(value);
|
return null;
|
||||||
this.props.hostSite.setPatchProperty(this.props.instanceId, this.props.propertyUri, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
handleClick(event: MouseEvent) {
|
||||||
if (!this.frameElement) {
|
if (!this.frameElement) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
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 enumeratedListContainerElement: HTMLElement | null = null;
|
||||||
private enumeratedListItemTemplate: string = "";
|
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) {
|
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]')
|
this.frameElement.querySelectorAll('[mod-role=enumeration-option]')
|
||||||
.forEach((inputControl: Element) => {
|
.forEach((inputControl: Element) => {
|
||||||
let strValue = inputControl.getAttribute("mod-port-value");
|
let strValue = inputControl.getAttribute("mod-port-value");
|
||||||
let value = parseFloat(strValue || "");
|
let value = parseFloat(strValue || "");
|
||||||
inputControl.classList.remove("selected");
|
inputControl.classList.remove("selected");
|
||||||
if (value === this.value) {
|
if (value === this.value) {
|
||||||
inputControl.classList.add("selected");
|
inputControl.classList.add("selected");
|
||||||
@@ -431,8 +653,7 @@ class CustomSelectControl implements ModGuiControl {
|
|||||||
if (role === "enumeration-option") // a click in the drop-down list?
|
if (role === "enumeration-option") // a click in the drop-down list?
|
||||||
{
|
{
|
||||||
let strValue = target.getAttribute("mod-port-value");
|
let strValue = target.getAttribute("mod-port-value");
|
||||||
if (strValue && strValue !== "")
|
if (strValue && strValue !== "") {
|
||||||
{
|
|
||||||
let value = parseFloat(strValue);
|
let value = parseFloat(strValue);
|
||||||
if (!isNaN(value)) {
|
if (!isNaN(value)) {
|
||||||
this.setControlValue(value);
|
this.setControlValue(value);
|
||||||
@@ -455,14 +676,7 @@ class CustomSelectControl implements ModGuiControl {
|
|||||||
|
|
||||||
interface BypassLightControlProps {
|
interface BypassLightControlProps {
|
||||||
instanceId: number;
|
instanceId: number;
|
||||||
pluginControl: UiControl;
|
model: PiPedalModel;
|
||||||
onValueChanged: (instanceId: number, symbol: string, value: number) => void;
|
|
||||||
monitorPort: (
|
|
||||||
instanceId: number,
|
|
||||||
symbol: string,
|
|
||||||
interval: number,
|
|
||||||
callback: (value: number) => void) => MonitorPortHandle;
|
|
||||||
unmonitorPort: (handle: MonitorPortHandle) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function ModMessage(message: string) {
|
function ModMessage(message: string) {
|
||||||
@@ -517,23 +731,22 @@ class BypassLightControl implements ModGuiControl {
|
|||||||
|
|
||||||
this.frameElement.classList.remove('on', 'off');
|
this.frameElement.classList.remove('on', 'off');
|
||||||
this.frameElement.classList.add(
|
this.frameElement.classList.add(
|
||||||
this.value === this.props.pluginControl.min_value ?
|
this.value ?
|
||||||
'on' : 'off'
|
'on' : 'off'
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
private mounted: boolean = false;
|
private mounted: boolean = false;
|
||||||
|
|
||||||
private monitorHandle: MonitorPortHandle | null = null;
|
private listenHandle: ListenHandle | null = null;
|
||||||
onMounted() {
|
onMounted() {
|
||||||
this.mounted = true;
|
this.mounted = true;
|
||||||
this.monitorHandle = this.props.monitorPort(
|
this.listenHandle = this.props.model.addPedalboardItemEnabledChangeListener(
|
||||||
this.props.instanceId,
|
this.props.instanceId,
|
||||||
this.props.pluginControl.symbol,
|
(instanceId: number, isEnabled: boolean) => {
|
||||||
1.0 / 15,
|
let bypass = isEnabled ? 1.0: 0.0;
|
||||||
(value: number) => {
|
if (bypass !== this.value) {
|
||||||
if (value != this.value) {
|
this.setControlValue(bypass);
|
||||||
this.setControlValue(value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -541,9 +754,9 @@ class BypassLightControl implements ModGuiControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onUnmount() {
|
onUnmount() {
|
||||||
if (this.monitorHandle) {
|
if (this.listenHandle) {
|
||||||
this.props.unmonitorPort(this.monitorHandle);
|
this.props.model.removePedalboardItemEnabledChangeListener(this.listenHandle);
|
||||||
this.monitorHandle = null;
|
this.listenHandle = null;
|
||||||
}
|
}
|
||||||
this.mounted = false;
|
this.mounted = false;
|
||||||
this.cancelRequestUpdate();
|
this.cancelRequestUpdate();
|
||||||
@@ -647,7 +860,7 @@ class FilmstripControl implements ModGuiControl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let range = this.valueToRange(this.value);
|
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;
|
return;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -712,9 +925,9 @@ class FilmstripControl implements ModGuiControl {
|
|||||||
let value = this.isPointerDown ? this.pointerDownValue : this.value;
|
let value = this.isPointerDown ? this.pointerDownValue : this.value;
|
||||||
value = this.clampValue(value);
|
value = this.clampValue(value);
|
||||||
|
|
||||||
let isHorizontalStrip =
|
let isHorizontalStrip =
|
||||||
this.filmstripInfo.frameWidth/this.filmstripInfo.frameHeight
|
this.filmstripInfo.frameWidth / this.filmstripInfo.frameHeight
|
||||||
< this.filmstripInfo.width/this.filmstripInfo.height;
|
< this.filmstripInfo.width / this.filmstripInfo.height;
|
||||||
if (isHorizontalStrip) {
|
if (isHorizontalStrip) {
|
||||||
let range = this.valueToRange(value);
|
let range = this.valueToRange(value);
|
||||||
|
|
||||||
@@ -1059,15 +1272,25 @@ class FilmstripControl implements ModGuiControl {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
function ModGuiHost(props: ModGuiHostProps) {
|
function ModGuiHost(props: ModGuiHostProps) {
|
||||||
let xmodel: PiPedalModel = PiPedalModelFactory.getInstance();
|
let model: PiPedalModel = PiPedalModelFactory.getInstance();
|
||||||
const { plugin, onClose } = props;
|
const { plugin, onClose } = props;
|
||||||
const [hostDivRef, setHostDivRef] = React.useState<HTMLDivElement | null>(null);
|
const [hostDivRef, setHostDivRef] = React.useState<HTMLDivElement | null>(null);
|
||||||
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
|
||||||
|
const [contentReady, setContentReady] = React.useState<boolean|null>(null);
|
||||||
const [modGuiControls] = React.useState<ModGuiControl[]>([]);
|
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) {
|
if (!plugin.modGui) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -1076,6 +1299,7 @@ function ModGuiHost(props: ModGuiHostProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function addPortClass(element: Element, selector: string, className: string) {
|
function addPortClass(element: Element, selector: string, className: string) {
|
||||||
let children = element.querySelectorAll(selector);
|
let children = element.querySelectorAll(selector);
|
||||||
// call addClass to each element that matches the selector
|
// call addClass to each element that matches the selector
|
||||||
@@ -1091,13 +1315,13 @@ function ModGuiHost(props: ModGuiHostProps) {
|
|||||||
pluginControl: pluginControl,
|
pluginControl: pluginControl,
|
||||||
onValueChanged: (instanceId: number, symbol: string, value: number) => {
|
onValueChanged: (instanceId: number, symbol: string, value: number) => {
|
||||||
try {
|
try {
|
||||||
props.hostSite.setPedalboardControl(instanceId, symbol, value);
|
model.setPedalboardControl(instanceId, symbol, value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
monitorPort: props.hostSite.monitorPort,
|
monitorPort: model.monitorPort.bind(model),
|
||||||
unmonitorPort: props.hostSite.unmonitorPort
|
unmonitorPort: model.unmonitorPort.bind(model)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
customSelectControl.attach(control as HTMLElement);
|
customSelectControl.attach(control as HTMLElement);
|
||||||
@@ -1108,6 +1332,20 @@ function ModGuiHost(props: ModGuiHostProps) {
|
|||||||
}
|
}
|
||||||
customSelectControl.onMounted();
|
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) {
|
function createFootswitchControl(control: Element, symbol: string, controlType: ControlType) {
|
||||||
let uiControl = new UiControl();
|
let uiControl = new UiControl();
|
||||||
uiControl.symbol = symbol;
|
uiControl.symbol = symbol;
|
||||||
@@ -1124,14 +1362,24 @@ function ModGuiHost(props: ModGuiHostProps) {
|
|||||||
filmStrip: "/img/footswitch_strip.png",
|
filmStrip: "/img/footswitch_strip.png",
|
||||||
verticalStrip: true,
|
verticalStrip: true,
|
||||||
onValueChanged: (instanceId: number, symbol: string, value: number) => {
|
onValueChanged: (instanceId: number, symbol: string, value: number) => {
|
||||||
try {
|
if (symbol === "_bypass") {
|
||||||
props.hostSite.setPedalboardControl(instanceId, symbol, value);
|
model.setPedalboardItemEnabled(instanceId, value !== 0);
|
||||||
} catch (error) {
|
} else {
|
||||||
|
try {
|
||||||
|
model.setPedalboardControl(instanceId, symbol, value);
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
monitorPort: props.hostSite.monitorPort,
|
monitorPort:
|
||||||
unmonitorPort: props.hostSite.unmonitorPort
|
symbol === "_bypass" ?
|
||||||
|
monitorBypassPort :
|
||||||
|
model.monitorPort.bind(model),
|
||||||
|
unmonitorPort:
|
||||||
|
symbol == "_bypass" ?
|
||||||
|
unmonitorBypassPort :
|
||||||
|
model.unmonitorPort.bind(model)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
filmstripControl.attach(control as HTMLElement);
|
filmstripControl.attach(control as HTMLElement);
|
||||||
@@ -1150,9 +1398,10 @@ function ModGuiHost(props: ModGuiHostProps) {
|
|||||||
}
|
}
|
||||||
let selectPathControl = new CustomSelectPathControl({
|
let selectPathControl = new CustomSelectPathControl({
|
||||||
instanceId: props.instanceId,
|
instanceId: props.instanceId,
|
||||||
propertyUri: pathUri,
|
propertyUri: pathUri,
|
||||||
plugin: props.plugin,
|
plugin: props.plugin,
|
||||||
hostSite: props.hostSite
|
hostSite: model,
|
||||||
|
handleFileSelect: props.handleFileSelect
|
||||||
});
|
});
|
||||||
|
|
||||||
selectPathControl.attach(control as HTMLElement);
|
selectPathControl.attach(control as HTMLElement);
|
||||||
@@ -1160,29 +1409,13 @@ function ModGuiHost(props: ModGuiHostProps) {
|
|||||||
selectPathControl.onMounted();
|
selectPathControl.onMounted();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function createBypassLightControl(control: Element, symbol: string) {
|
function createBypassLightControl(control: Element) {
|
||||||
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;
|
|
||||||
|
|
||||||
let bypassLightControl = new BypassLightControl(
|
let bypassLightControl = new BypassLightControl(
|
||||||
{
|
{
|
||||||
instanceId: props.instanceId,
|
instanceId: props.instanceId,
|
||||||
pluginControl: uiControl,
|
model: model
|
||||||
onValueChanged: (instanceId: number, symbol: string, value: number) => {
|
|
||||||
try {
|
|
||||||
props.hostSite.setPedalboardControl(instanceId, symbol, value);
|
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
monitorPort: props.hostSite.monitorPort,
|
|
||||||
unmonitorPort: props.hostSite.unmonitorPort
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
bypassLightControl.attach(control as HTMLElement);
|
bypassLightControl.attach(control as HTMLElement);
|
||||||
@@ -1211,13 +1444,13 @@ function ModGuiHost(props: ModGuiHostProps) {
|
|||||||
verticalStrip: false,
|
verticalStrip: false,
|
||||||
onValueChanged: (instanceId: number, symbol: string, value: number) => {
|
onValueChanged: (instanceId: number, symbol: string, value: number) => {
|
||||||
try {
|
try {
|
||||||
props.hostSite.setPedalboardControl(instanceId, symbol, value);
|
model.setPedalboardControl(instanceId, symbol, value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
monitorPort: props.hostSite.monitorPort,
|
monitorPort: model.monitorPort.bind(model),
|
||||||
unmonitorPort: props.hostSite.unmonitorPort
|
unmonitorPort: model.unmonitorPort.bind(model)
|
||||||
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -1284,14 +1517,11 @@ function ModGuiHost(props: ModGuiHostProps) {
|
|||||||
.forEach((control) => {
|
.forEach((control) => {
|
||||||
let symbol = "_bypass";
|
let symbol = "_bypass";
|
||||||
let controlType = ControlType.OnOffSwitch;
|
let controlType = ControlType.OnOffSwitch;
|
||||||
|
|
||||||
createFootswitchControl(control, symbol, controlType);
|
createFootswitchControl(control, symbol, controlType);
|
||||||
});
|
});
|
||||||
element.querySelectorAll('[mod-role=bypass-light]')
|
element.querySelectorAll('[mod-role=bypass-light]')
|
||||||
.forEach((control) => {
|
.forEach((control) => {
|
||||||
let symbol = "_bypass";
|
createBypassLightControl(control);
|
||||||
|
|
||||||
createBypassLightControl(control, symbol);
|
|
||||||
});
|
});
|
||||||
element.querySelectorAll('[mod-role=input-parameter]')
|
element.querySelectorAll('[mod-role=input-parameter]')
|
||||||
.forEach((control) => {
|
.forEach((control) => {
|
||||||
@@ -1303,6 +1533,7 @@ function ModGuiHost(props: ModGuiHostProps) {
|
|||||||
setErrorMessage(ModMessage(message));
|
setErrorMessage(ModMessage(message));
|
||||||
}
|
}
|
||||||
async function requestContent() {
|
async function requestContent() {
|
||||||
|
updateContentReady(false);
|
||||||
try {
|
try {
|
||||||
let modGui = plugin.modGui;
|
let modGui = plugin.modGui;
|
||||||
if (!modGui) {
|
if (!modGui) {
|
||||||
@@ -1321,7 +1552,7 @@ function ModGuiHost(props: ModGuiHostProps) {
|
|||||||
let encodedUri = encodeURIComponent(props.plugin.uri);
|
let encodedUri = encodeURIComponent(props.plugin.uri);
|
||||||
let version = props.plugin.minorVersion * 1000 + props.plugin.microVersion;
|
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 queryParams = "?ns=" + encodedUri + "&v=" + version.toString();
|
||||||
let templateUri = resourceUrl + "_/iconTemplate" + queryParams;
|
let templateUri = resourceUrl + "_/iconTemplate" + queryParams;
|
||||||
let cssUri = resourceUrl + "_/stylesheet" + queryParams;
|
let cssUri = resourceUrl + "_/stylesheet" + queryParams;
|
||||||
@@ -1367,6 +1598,7 @@ function ModGuiHost(props: ModGuiHostProps) {
|
|||||||
let height = element.clientHeight;
|
let height = element.clientHeight;
|
||||||
hostDivRef.style.width = width + "px";
|
hostDivRef.style.width = width + "px";
|
||||||
hostDivRef.style.height = height + "px";
|
hostDivRef.style.height = height + "px";
|
||||||
|
updateContentReady(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setModError("Error loading UI: " + (error instanceof Error ? error.message : String(error)));
|
setModError("Error loading UI: " + (error instanceof Error ? error.message : String(error)));
|
||||||
}
|
}
|
||||||
@@ -1383,9 +1615,12 @@ function ModGuiHost(props: ModGuiHostProps) {
|
|||||||
let stateHandler = (state: State) => {
|
let stateHandler = (state: State) => {
|
||||||
setReady(state === State.Ready);
|
setReady(state === State.Ready);
|
||||||
}
|
}
|
||||||
xmodel.state.addOnChangedHandler(stateHandler)
|
model.state.addOnChangedHandler(stateHandler)
|
||||||
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
xmodel.state.removeOnChangedHandler(stateHandler);
|
model.state.removeOnChangedHandler(stateHandler);
|
||||||
|
|
||||||
if (tHostDivRef !== null) {
|
if (tHostDivRef !== null) {
|
||||||
// unmount the custom content.
|
// unmount the custom content.
|
||||||
let children = tHostDivRef.children;
|
let children = tHostDivRef.children;
|
||||||
@@ -1396,6 +1631,7 @@ function ModGuiHost(props: ModGuiHostProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
updateContentReady(false);
|
||||||
let mc = modGuiControls;
|
let mc = modGuiControls;
|
||||||
for (let i = 0; i < mc.length; i++) {
|
for (let i = 0; i < mc.length; i++) {
|
||||||
let modGuiControl = mc[i];
|
let modGuiControl = mc[i];
|
||||||
@@ -1405,12 +1641,12 @@ function ModGuiHost(props: ModGuiHostProps) {
|
|||||||
};
|
};
|
||||||
}, [hostDivRef, plugin, ready]);
|
}, [hostDivRef, plugin, ready]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModGuiErrorBoundary plugin={props.plugin} onClose={() => { props.onClose(); setErrorMessage(null); }}>
|
<ModGuiErrorBoundary plugin={props.plugin} onClose={() => { props.onClose(); setErrorMessage(null); }}>
|
||||||
<div style={{
|
<div style={{
|
||||||
position: "absolute", top: 0, left: 0, right: 0, bottom: 0,
|
display: "inline-block",
|
||||||
backgroundColor: "rgba(0.8,0.8,0.8, 0.8)",
|
paddingLeft: 20, paddingRight: 20,
|
||||||
display: "flex", flexFlow: "column nowrap", alignItems: "center", justifyContent: "stretch",
|
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
@@ -1418,14 +1654,14 @@ function ModGuiHost(props: ModGuiHostProps) {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconButtonEx tooltip="Close" onClick={() => onClose()} style={{
|
{maximizedUi && (
|
||||||
position: "absolute", top: 16, right: 16,
|
<IconButtonEx tooltip="Close" onClick={() => onClose()} style={{
|
||||||
background: "#FFF5", zIndex: 600
|
position: "absolute", top: 16, right: 16,
|
||||||
}}>
|
background: "#FFF5", zIndex: 600
|
||||||
<CloseIcon />
|
}}>
|
||||||
</IconButtonEx>
|
<CloseIcon />
|
||||||
|
</IconButtonEx>
|
||||||
<div style={{ flex: "1 1 1px" }} />
|
)}
|
||||||
|
|
||||||
<div style={{ display: "block", position: "relative" }}>
|
<div style={{ display: "block", position: "relative" }}>
|
||||||
<div ref={setHostDivRef} style={{ display: "block", position: "relative" }}
|
<div ref={setHostDivRef} style={{ display: "block", position: "relative" }}
|
||||||
@@ -1437,17 +1673,78 @@ function ModGuiHost(props: ModGuiHostProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ flex: "2 2 1px" }} />
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<OkDialog title="Error" open={errorMessage !== null}
|
{errorMessage !== null && (
|
||||||
onClose={() => {
|
<OkDialog title="Error" open={errorMessage !== null}
|
||||||
setErrorMessage(null);
|
onClose={() => {
|
||||||
onClose();
|
setErrorMessage(null);
|
||||||
}}
|
onClose();
|
||||||
text={errorMessage ?? ""}
|
}}
|
||||||
/>
|
text={errorMessage ?? ""}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
</ModGuiErrorBoundary>
|
</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;
|
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.lv2State = input.lv2State;
|
||||||
this.lilvPresetUri = input.lilvPresetUri;
|
this.lilvPresetUri = input.lilvPresetUri;
|
||||||
this.pathProperties = input.pathProperties;
|
this.pathProperties = input.pathProperties;
|
||||||
|
this.useModUi = input.useModUi ?? false;
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
deserialize(input: any): PedalboardItem {
|
deserialize(input: any): PedalboardItem {
|
||||||
@@ -212,6 +214,7 @@ export class PedalboardItem implements Deserializable<PedalboardItem> {
|
|||||||
lv2State: [boolean,any] = [false,{}];
|
lv2State: [boolean,any] = [false,{}];
|
||||||
lilvPresetUri: string = "";
|
lilvPresetUri: string = "";
|
||||||
pathProperties: {[Name: string]: string} = {};
|
pathProperties: {[Name: string]: string} = {};
|
||||||
|
useModUi: boolean = false; // true if this item should use the mod-ui.
|
||||||
};
|
};
|
||||||
|
|
||||||
export class SnapshotValue {
|
export class SnapshotValue {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ import FilePropertyDirectoryTree from './FilePropertyDirectoryTree';
|
|||||||
import AudioFileMetadata from './AudioFileMetadata';
|
import AudioFileMetadata from './AudioFileMetadata';
|
||||||
import { pathFileName } from './FileUtils';
|
import { pathFileName } from './FileUtils';
|
||||||
import { AlsaSequencerConfiguration, AlsaSequencerPortSelection } from './AlsaSequencer';
|
import { AlsaSequencerConfiguration, AlsaSequencerPortSelection } from './AlsaSequencer';
|
||||||
|
import {getDefaultModGuiPreference} from './ModGuiHost';
|
||||||
|
|
||||||
export enum State {
|
export enum State {
|
||||||
Loading,
|
Loading,
|
||||||
@@ -82,6 +82,24 @@ export enum ReconnectReason {
|
|||||||
HotspotChanging
|
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 type ControlValueChangedHandler = (key: string, value: number) => void;
|
||||||
|
|
||||||
export interface FileEntry {
|
export interface FileEntry {
|
||||||
@@ -146,7 +164,6 @@ export interface VuUpdateInfo {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface MonitorPortHandle {
|
export interface MonitorPortHandle {
|
||||||
|
|
||||||
};
|
};
|
||||||
export interface ControlValueChangedHandle {
|
export interface ControlValueChangedHandle {
|
||||||
_ControlValueChangedHandle: number;
|
_ControlValueChangedHandle: number;
|
||||||
@@ -574,10 +591,18 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
return true;
|
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) {
|
private setModelPedalboard(pedalboard: Pedalboard) {
|
||||||
this.pedalboard.set(pedalboard);
|
this.pedalboard.set(pedalboard);
|
||||||
this.selectedSnapshot.set(pedalboard.selectedSnapshot);
|
this.selectedSnapshot.set(pedalboard.selectedSnapshot);
|
||||||
|
this.updateEnabledItems(pedalboard);
|
||||||
}
|
}
|
||||||
onSocketMessage(header: PiPedalMessageHeader, body?: any) {
|
onSocketMessage(header: PiPedalMessageHeader, body?: any) {
|
||||||
|
|
||||||
@@ -687,6 +712,13 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
itemEnabledBody.enabled,
|
itemEnabledBody.enabled,
|
||||||
false // No server notification.
|
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") {
|
} else if (message === "onPedalboardChanged") {
|
||||||
let pedalChangedBody = body as PedalboardChangedBody;
|
let pedalChangedBody = body as PedalboardChangedBody;
|
||||||
this.setModelPedalboard(new Pedalboard().deserialize(pedalChangedBody.pedalboard));
|
this.setModelPedalboard(new Pedalboard().deserialize(pedalChangedBody.pedalboard));
|
||||||
@@ -1563,6 +1595,42 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
setPedalboardItemEnabled(instanceId: number, value: boolean): void {
|
setPedalboardItemEnabled(instanceId: number, value: boolean): void {
|
||||||
this._setPedalboardItemEnabled(instanceId, value, true);
|
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 {
|
private _setPedalboardItemEnabled(instanceId: number, value: boolean, notifyServer: boolean): void {
|
||||||
let pedalboard = this.pedalboard.get();
|
let pedalboard = this.pedalboard.get();
|
||||||
if (pedalboard === undefined) throw new PiPedalStateError("Pedalboard not ready.");
|
if (pedalboard === undefined) throw new PiPedalStateError("Pedalboard not ready.");
|
||||||
@@ -1625,6 +1693,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
item.lv2State = [false, {}];
|
item.lv2State = [false, {}];
|
||||||
item.vstState = "";
|
item.vstState = "";
|
||||||
item.pathProperties = {};
|
item.pathProperties = {};
|
||||||
|
item.useModUi = getDefaultModGuiPreference(selectedUri);
|
||||||
for (let fileProperty of plugin.fileProperties) {
|
for (let fileProperty of plugin.fileProperties) {
|
||||||
// stringized json for an atom. see AtomConverter.hpp.
|
// stringized json for an atom. see AtomConverter.hpp.
|
||||||
// null -> we've never seen a value.
|
// null -> we've never seen a value.
|
||||||
@@ -1845,6 +1914,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
newPedalboard.setItemEmpty(item);
|
newPedalboard.setItemEmpty(item);
|
||||||
|
|
||||||
|
|
||||||
this.setModelPedalboard(newPedalboard);
|
this.setModelPedalboard(newPedalboard);
|
||||||
this.updateServerPedalboard();
|
this.updateServerPedalboard();
|
||||||
return item.instanceId;
|
return item.instanceId;
|
||||||
@@ -2210,6 +2280,18 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
getPatchProperty<Type = any>(instanceId: number, uri: string): Promise<Type> {
|
getPatchProperty<Type = any>(instanceId: number, uri: string): Promise<Type> {
|
||||||
let result = new Promise<Type>((resolve, reject) => {
|
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) {
|
if (!this.webSocket) {
|
||||||
reject("Socket closed.");
|
reject("Socket closed.");
|
||||||
} else {
|
} else {
|
||||||
@@ -3233,6 +3315,94 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
throw new Error("No connection.");
|
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;
|
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
|
// 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
|
// 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 { createStyles } from './WithStyles';
|
||||||
import { css } from '@emotion/react';
|
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 { withStyles } from "tss-react/mui";
|
||||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||||
@@ -353,7 +355,9 @@ type PluginControlViewState = {
|
|||||||
imeInitialHeight: number;
|
imeInitialHeight: number;
|
||||||
showFileDialog: boolean,
|
showFileDialog: boolean,
|
||||||
dialogFileProperty: UiFileProperty,
|
dialogFileProperty: UiFileProperty,
|
||||||
dialogFileValue: string
|
dialogFileValue: string,
|
||||||
|
modGuiContentReady: boolean,
|
||||||
|
showModGuiZoomed: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PluginControlView =
|
const PluginControlView =
|
||||||
@@ -372,7 +376,9 @@ const PluginControlView =
|
|||||||
imeInitialHeight: 0,
|
imeInitialHeight: 0,
|
||||||
showFileDialog: false,
|
showFileDialog: false,
|
||||||
dialogFileProperty: new UiFileProperty(),
|
dialogFileProperty: new UiFileProperty(),
|
||||||
dialogFileValue: ""
|
dialogFileValue: "",
|
||||||
|
modGuiContentReady: false,
|
||||||
|
showModGuiZoomed: false
|
||||||
|
|
||||||
}
|
}
|
||||||
this.onPedalboardChanged = this.onPedalboardChanged.bind(this);
|
this.onPedalboardChanged = this.onPedalboardChanged.bind(this);
|
||||||
@@ -774,36 +780,64 @@ const PluginControlView =
|
|||||||
}
|
}
|
||||||
return false;
|
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 {
|
renderModGui(): ReactNode {
|
||||||
|
const classes = withStyles.getClasses(this.props);
|
||||||
|
void classes;
|
||||||
|
|
||||||
let uiPlugin = this.model.getUiPlugin(this.props.item.uri);
|
let uiPlugin = this.model.getUiPlugin(this.props.item.uri);
|
||||||
if (!uiPlugin) {
|
if (!uiPlugin) {
|
||||||
return (<div />);
|
return (<div />);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<ModGuiHost
|
<div style={{ width: "100%", height: "100%", }}>
|
||||||
instanceId={this.props.instanceId}
|
<AutoZoom leftPad={36} rightPad={52} contentReady={this.state.modGuiContentReady}
|
||||||
plugin={uiPlugin}
|
showZoomed={this.state.showModGuiZoomed}
|
||||||
onClose={() => {
|
setShowZoomed={
|
||||||
if (this.props.onSetShowModGui) {
|
(value) => { this.setState({ showModGuiZoomed: value }); }
|
||||||
this.props.onSetShowModGui(this.props.instanceId, false);
|
|
||||||
}
|
}
|
||||||
}}
|
>
|
||||||
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));
|
nodes.push(this.midiBindingControl(pedalboardItem));
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={scrollClass}>
|
<div className={scrollClass}>
|
||||||
<div className={gridClass} >
|
<div className={gridClass} >
|
||||||
{
|
{
|
||||||
nodes
|
nodes
|
||||||
}
|
}
|
||||||
{/* Extra space to allow scrolling right to the end in lascape especially */}
|
{/* Extra space to allow scrolling right to the end in lascape especially */}
|
||||||
{!this.fullScreen() && (
|
{!this.fullScreen() && (
|
||||||
<div style={{ flex: "0 0 40px", width: 40, height: 40 }} />
|
<div style={{ flex: "0 0 40px", width: 40, height: 40 }} />
|
||||||
)}
|
)}
|
||||||
{
|
{
|
||||||
(!this.state.landscapeGrid) && (!this.fullScreen()) && (
|
(!this.state.landscapeGrid) && (!this.fullScreen()) && (
|
||||||
<div style={{ flex: "0 1 100%", width: "0px", height: 40 }} />
|
<div style={{ flex: "0 1 100%", width: "0px", height: 40 }} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
@@ -895,7 +929,7 @@ const PluginControlView =
|
|||||||
let vuMeterRClass = this.state.landscapeGrid ? classes.vuMeterRLandscape : classes.vuMeterR;
|
let vuMeterRClass = this.state.landscapeGrid ? classes.vuMeterRLandscape : classes.vuMeterR;
|
||||||
let frameClass = classes.frame;
|
let frameClass = classes.frame;
|
||||||
|
|
||||||
if (this.fullScreen()) {
|
if (this.fullScreen() || this.props.showModGui) {
|
||||||
frameClass = classes.noScrollFrame;
|
frameClass = classes.noScrollFrame;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -920,13 +954,15 @@ const PluginControlView =
|
|||||||
instanceId={this.props.instanceId}
|
instanceId={this.props.instanceId}
|
||||||
selectedFile={this.state.dialogFileValue}
|
selectedFile={this.state.dialogFileValue}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
this.setState({ showFileDialog: false });
|
this.setState({
|
||||||
|
showFileDialog: false
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
onApply={(fileProperty, selectedFile) => {
|
onApply={(fileProperty, selectedFile) => {
|
||||||
this.model.setPatchProperty(
|
this.model.setPatchProperty(
|
||||||
this.props.instanceId,
|
this.props.instanceId,
|
||||||
fileProperty.patchProperty,
|
fileProperty.patchProperty,
|
||||||
JsonAtom.Path(selectedFile)
|
JsonAtom._Path(selectedFile).asAny()
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
||||||
@@ -941,7 +977,7 @@ const PluginControlView =
|
|||||||
this.model.setPatchProperty(
|
this.model.setPatchProperty(
|
||||||
this.props.instanceId,
|
this.props.instanceId,
|
||||||
fileProperty.patchProperty,
|
fileProperty.patchProperty,
|
||||||
JsonAtom.Path(selectedFile)
|
JsonAtom._Path(selectedFile).asAny()
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class Rect {
|
|||||||
this.x = x; this.y = y; this.width = width; this.height = height;
|
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 {
|
copy(): Rect {
|
||||||
let result = new Rect();
|
let result = new Rect();
|
||||||
result.x = this.x;
|
result.x = this.x;
|
||||||
@@ -84,6 +84,12 @@ class Rect {
|
|||||||
this.x += xOffset;
|
this.x += xOffset;
|
||||||
this.y += yOffset;
|
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;
|
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
|
// 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
|
// this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ const ToobCabSimView =
|
|||||||
item={this.props.item}
|
item={this.props.item}
|
||||||
customization={this}
|
customization={this}
|
||||||
customizationId={this.customizationId}
|
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 { Theme } from '@mui/material/styles';
|
||||||
|
|
||||||
import WithStyles from './WithStyles';
|
import WithStyles from './WithStyles';
|
||||||
import {createStyles} from './WithStyles';
|
import { createStyles } from './WithStyles';
|
||||||
|
|
||||||
import { withStyles } from "tss-react/mui";
|
import { withStyles } from "tss-react/mui";
|
||||||
|
|
||||||
import IControlViewFactory from './IControlViewFactory';
|
import IControlViewFactory from './IControlViewFactory';
|
||||||
import { PiPedalModelFactory, PiPedalModel } from "./PiPedalModel";
|
import { PiPedalModelFactory, PiPedalModel } from "./PiPedalModel";
|
||||||
import { PedalboardItem } from './Pedalboard';
|
import { PedalboardItem } from './Pedalboard';
|
||||||
import PluginControlView, { ControlGroup,ControlViewCustomization } from './PluginControlView';
|
import PluginControlView, { ControlGroup, ControlViewCustomization } from './PluginControlView';
|
||||||
//import ToobFrequencyResponseView from './ToobFrequencyResponseView';
|
//import ToobFrequencyResponseView from './ToobFrequencyResponseView';
|
||||||
|
|
||||||
|
|
||||||
@@ -47,12 +47,11 @@ interface ToobInputStageState {
|
|||||||
|
|
||||||
const ToobInputStageView =
|
const ToobInputStageView =
|
||||||
withStyles(
|
withStyles(
|
||||||
class extends React.Component<ToobInputStageProps, ToobInputStageState>
|
class extends React.Component<ToobInputStageProps, ToobInputStageState>
|
||||||
implements ControlViewCustomization
|
implements ControlViewCustomization {
|
||||||
{
|
|
||||||
model: PiPedalModel;
|
model: PiPedalModel;
|
||||||
|
|
||||||
customizationId: number = 1;
|
customizationId: number = 1;
|
||||||
|
|
||||||
constructor(props: ToobInputStageProps) {
|
constructor(props: ToobInputStageProps) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -64,8 +63,7 @@ const ToobInputStageView =
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
modifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
modifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] {
|
||||||
{
|
|
||||||
return controls;
|
return controls;
|
||||||
// let group = controls[1] as ControlGroup;
|
// let group = controls[1] as ControlGroup;
|
||||||
// group.controls.splice(0,0,
|
// group.controls.splice(0,0,
|
||||||
@@ -79,6 +77,9 @@ const ToobInputStageView =
|
|||||||
item={this.props.item}
|
item={this.props.item}
|
||||||
customization={this}
|
customization={this}
|
||||||
customizationId={this.customizationId}
|
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 { Theme } from '@mui/material/styles';
|
||||||
|
|
||||||
import WithStyles from './WithStyles';
|
import WithStyles from './WithStyles';
|
||||||
import {createStyles} from './WithStyles';
|
import { createStyles } from './WithStyles';
|
||||||
|
|
||||||
import { withStyles } from "tss-react/mui";
|
import { withStyles } from "tss-react/mui";
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ const ToobMLView =
|
|||||||
class extends React.Component<ToobMLProps, ToobMLState>
|
class extends React.Component<ToobMLProps, ToobMLState>
|
||||||
implements ControlViewCustomization {
|
implements ControlViewCustomization {
|
||||||
model: PiPedalModel;
|
model: PiPedalModel;
|
||||||
gainRef: React.RefObject<HTMLDivElement|null>;
|
gainRef: React.RefObject<HTMLDivElement | null>;
|
||||||
|
|
||||||
customizationId: number = 1;
|
customizationId: number = 1;
|
||||||
|
|
||||||
@@ -119,19 +119,17 @@ const ToobMLView =
|
|||||||
// }
|
// }
|
||||||
let controlIndex: number = -1;
|
let controlIndex: number = -1;
|
||||||
let controlValues = this.props.item.controlValues
|
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 ctl: any = controls[i];
|
||||||
let key: any = ctl?.props?.uiControl?.symbol??"";
|
let key: any = ctl?.props?.uiControl?.symbol ?? "";
|
||||||
|
|
||||||
if (key === "gain")
|
if (key === "gain") {
|
||||||
{
|
|
||||||
controlIndex = i;
|
controlIndex = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (controlIndex !== -1 ) {
|
if (controlIndex !== -1) {
|
||||||
|
|
||||||
let gainControl: React.ReactElement = controls[controlIndex] as React.ReactElement;
|
let gainControl: React.ReactElement = controls[controlIndex] as React.ReactElement;
|
||||||
controls[controlIndex] = (<div ref={this.gainRef}> {gainControl} </div>);
|
controls[controlIndex] = (<div ref={this.gainRef}> {gainControl} </div>);
|
||||||
@@ -144,6 +142,9 @@ const ToobMLView =
|
|||||||
item={this.props.item}
|
item={this.props.item}
|
||||||
customization={this}
|
customization={this}
|
||||||
customizationId={this.customizationId}
|
customizationId={this.customizationId}
|
||||||
|
showModGui={false}
|
||||||
|
onSetShowModGui={(instanceId: number, showModGui: boolean) => { }}
|
||||||
|
|
||||||
/>);
|
/>);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -525,7 +525,7 @@ export default function ToobPlayerControl(
|
|||||||
function onNextTrack() {
|
function onNextTrack() {
|
||||||
model.getNextAudioFile(audioFile)
|
model.getNextAudioFile(audioFile)
|
||||||
.then((file) => {
|
.then((file) => {
|
||||||
let json = JsonAtom.Path(file);
|
let json = JsonAtom._Path(file).asAny();
|
||||||
model.setPatchProperty(
|
model.setPatchProperty(
|
||||||
props.instanceId,
|
props.instanceId,
|
||||||
AUDIO_FILE_PROPERTY_URI,
|
AUDIO_FILE_PROPERTY_URI,
|
||||||
@@ -538,7 +538,7 @@ export default function ToobPlayerControl(
|
|||||||
function onPreviousTrack() {
|
function onPreviousTrack() {
|
||||||
model.getPreviousAudioFile(audioFile)
|
model.getPreviousAudioFile(audioFile)
|
||||||
.then((file) => {
|
.then((file) => {
|
||||||
let json = JsonAtom.Path(file);
|
let json = JsonAtom._Path(file).asAny();
|
||||||
model.setPatchProperty(
|
model.setPatchProperty(
|
||||||
props.instanceId,
|
props.instanceId,
|
||||||
AUDIO_FILE_PROPERTY_URI,
|
AUDIO_FILE_PROPERTY_URI,
|
||||||
@@ -873,7 +873,7 @@ export default function ToobPlayerControl(
|
|||||||
model.setPatchProperty(
|
model.setPatchProperty(
|
||||||
props.instanceId,
|
props.instanceId,
|
||||||
AUDIO_FILE_PROPERTY_URI,
|
AUDIO_FILE_PROPERTY_URI,
|
||||||
JsonAtom.Path(selectedFile)
|
JsonAtom._Path(selectedFile).asAny()
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
||||||
@@ -888,7 +888,7 @@ export default function ToobPlayerControl(
|
|||||||
model.setPatchProperty(
|
model.setPatchProperty(
|
||||||
props.instanceId,
|
props.instanceId,
|
||||||
fileProperty.patchProperty,
|
fileProperty.patchProperty,
|
||||||
JsonAtom.Path(selectedFile)
|
JsonAtom._Path(selectedFile).asAny()
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
||||||
|
|||||||
@@ -110,14 +110,14 @@ const ToobPlayerView =
|
|||||||
for (let mixControl of mixPanel.controls) {
|
for (let mixControl of mixPanel.controls) {
|
||||||
extraControls.push(
|
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}
|
{mixControl}
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let panel = (
|
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)[] = [];
|
let result: (React.ReactNode | ControlGroup)[] = [];
|
||||||
@@ -133,6 +133,9 @@ const ToobPlayerView =
|
|||||||
item={this.props.item}
|
item={this.props.item}
|
||||||
customization={this}
|
customization={this}
|
||||||
customizationId={this.customizationId}
|
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";
|
uri: string = "http://two-play.com/plugins/toob-player";
|
||||||
|
|
||||||
Create(model: PiPedalModel, pedalboardItem: PedalboardItem): React.ReactNode {
|
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}
|
item={this.props.item}
|
||||||
customization={this}
|
customization={this}
|
||||||
customizationId={this.customizationId}
|
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 { Theme } from '@mui/material/styles';
|
||||||
|
|
||||||
import WithStyles from './WithStyles';
|
import WithStyles from './WithStyles';
|
||||||
import {createStyles} from './WithStyles';
|
import { createStyles } from './WithStyles';
|
||||||
|
|
||||||
import { withStyles } from "tss-react/mui";
|
import { withStyles } from "tss-react/mui";
|
||||||
|
|
||||||
import IControlViewFactory from './IControlViewFactory';
|
import IControlViewFactory from './IControlViewFactory';
|
||||||
import { PiPedalModelFactory, PiPedalModel } from "./PiPedalModel";
|
import { PiPedalModelFactory, PiPedalModel } from "./PiPedalModel";
|
||||||
import { PedalboardItem } from './Pedalboard';
|
import { PedalboardItem } from './Pedalboard';
|
||||||
import PluginControlView, { ControlGroup,ControlViewCustomization } from './PluginControlView';
|
import PluginControlView, { ControlGroup, ControlViewCustomization } from './PluginControlView';
|
||||||
import ToobSpectrumResponseView from './ToobSpectrumResponseView';
|
import ToobSpectrumResponseView from './ToobSpectrumResponseView';
|
||||||
|
|
||||||
|
|
||||||
@@ -48,12 +48,11 @@ interface ToobSpectrumAnalyzerState {
|
|||||||
|
|
||||||
const ToobSpectrumAnalyzerView =
|
const ToobSpectrumAnalyzerView =
|
||||||
withStyles(
|
withStyles(
|
||||||
class extends React.Component<ToobSpectrumAnalyzerProps, ToobSpectrumAnalyzerState>
|
class extends React.Component<ToobSpectrumAnalyzerProps, ToobSpectrumAnalyzerState>
|
||||||
implements ControlViewCustomization
|
implements ControlViewCustomization {
|
||||||
{
|
|
||||||
model: PiPedalModel;
|
model: PiPedalModel;
|
||||||
|
|
||||||
customizationId: number = 1;
|
customizationId: number = 1;
|
||||||
|
|
||||||
constructor(props: ToobSpectrumAnalyzerProps) {
|
constructor(props: ToobSpectrumAnalyzerProps) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -65,11 +64,10 @@ const ToobSpectrumAnalyzerView =
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
modifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
modifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] {
|
||||||
{
|
controls.splice(0, 0,
|
||||||
controls.splice(0,0,
|
(<ToobSpectrumResponseView instanceId={this.props.instanceId} />)
|
||||||
( <ToobSpectrumResponseView instanceId={this.props.instanceId} />)
|
);
|
||||||
);
|
|
||||||
return controls;
|
return controls;
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
@@ -78,6 +76,9 @@ const ToobSpectrumAnalyzerView =
|
|||||||
item={this.props.item}
|
item={this.props.item}
|
||||||
customization={this}
|
customization={this}
|
||||||
customizationId={this.customizationId}
|
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 { Theme } from '@mui/material/styles';
|
||||||
|
|
||||||
import WithStyles from './WithStyles';
|
import WithStyles from './WithStyles';
|
||||||
import {createStyles} from './WithStyles';
|
import { createStyles } from './WithStyles';
|
||||||
|
|
||||||
import { withStyles } from "tss-react/mui";
|
import { withStyles } from "tss-react/mui";
|
||||||
|
|
||||||
import IControlViewFactory from './IControlViewFactory';
|
import IControlViewFactory from './IControlViewFactory';
|
||||||
import { PiPedalModelFactory, PiPedalModel,ControlValueChangedHandle } from "./PiPedalModel";
|
import { PiPedalModelFactory, PiPedalModel, ControlValueChangedHandle } from "./PiPedalModel";
|
||||||
import { PedalboardItem } from './Pedalboard';
|
import { PedalboardItem } from './Pedalboard';
|
||||||
import PluginControlView, { ControlGroup,ControlViewCustomization } from './PluginControlView';
|
import PluginControlView, { ControlGroup, ControlViewCustomization } from './PluginControlView';
|
||||||
import ToobFrequencyResponseView from './ToobFrequencyResponseView';
|
import ToobFrequencyResponseView from './ToobFrequencyResponseView';
|
||||||
|
|
||||||
|
|
||||||
@@ -47,12 +47,11 @@ interface ToobToneStackState {
|
|||||||
|
|
||||||
const ToobToneStackView =
|
const ToobToneStackView =
|
||||||
withStyles(
|
withStyles(
|
||||||
class extends React.Component<ToobToneStackProps, ToobToneStackState>
|
class extends React.Component<ToobToneStackProps, ToobToneStackState>
|
||||||
implements ControlViewCustomization
|
implements ControlViewCustomization {
|
||||||
{
|
|
||||||
model: PiPedalModel;
|
model: PiPedalModel;
|
||||||
|
|
||||||
customizationId: number = 1;
|
customizationId: number = 1;
|
||||||
|
|
||||||
constructor(props: ToobToneStackProps) {
|
constructor(props: ToobToneStackProps) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -61,27 +60,24 @@ const ToobToneStackView =
|
|||||||
isBaxandall: this.IsBaxandall()
|
isBaxandall: this.IsBaxandall()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IsBaxandall() : boolean {
|
IsBaxandall(): boolean {
|
||||||
return this.props.item.getControl("ampmodel").value === 2.0;
|
return this.props.item.getControl("ampmodel").value === 2.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
controlValueChangedHandle?: ControlValueChangedHandle;
|
controlValueChangedHandle?: ControlValueChangedHandle;
|
||||||
componentDidMount()
|
componentDidMount() {
|
||||||
{
|
|
||||||
this.controlValueChangedHandle = this.model.addControlValueChangeListener(
|
this.controlValueChangedHandle = this.model.addControlValueChangeListener(
|
||||||
this.props.instanceId,
|
this.props.instanceId,
|
||||||
(key,value) => {
|
(key, value) => {
|
||||||
if (key === "ampmodel")
|
if (key === "ampmodel") {
|
||||||
{
|
this.setState({ isBaxandall: value === 2.0 });
|
||||||
this.setState({isBaxandall: value === 2.0});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
if (this.controlValueChangedHandle)
|
if (this.controlValueChangedHandle) {
|
||||||
{
|
|
||||||
this.model.removeControlValueChangeListener(this.controlValueChangedHandle);
|
this.model.removeControlValueChangeListener(this.controlValueChangedHandle);
|
||||||
this.controlValueChangedHandle = undefined;
|
this.controlValueChangedHandle = undefined;
|
||||||
}
|
}
|
||||||
@@ -90,18 +86,16 @@ const ToobToneStackView =
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
modifyControls(controls: (React.ReactNode| ControlGroup)[]): (React.ReactNode| ControlGroup)[]
|
modifyControls(controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] {
|
||||||
{
|
if (this.state.isBaxandall) {
|
||||||
if (this.state.isBaxandall)
|
controls.splice(0, 0,
|
||||||
{
|
(<ToobFrequencyResponseView instanceId={this.props.instanceId}
|
||||||
controls.splice(0,0,
|
/>)
|
||||||
( <ToobFrequencyResponseView instanceId={this.props.instanceId}
|
);
|
||||||
/>)
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
controls.splice(0,0,
|
controls.splice(0, 0,
|
||||||
( <ToobFrequencyResponseView instanceId={this.props.instanceId} />)
|
(<ToobFrequencyResponseView instanceId={this.props.instanceId} />)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return controls;
|
return controls;
|
||||||
}
|
}
|
||||||
@@ -111,6 +105,9 @@ const ToobToneStackView =
|
|||||||
item={this.props.item}
|
item={this.props.item}
|
||||||
customization={this}
|
customization={this}
|
||||||
customizationId={this.customizationId}
|
customizationId={this.customizationId}
|
||||||
|
showModGui={false}
|
||||||
|
onSetShowModGui={(instanceId: number, showModGui: boolean) => { }}
|
||||||
|
|
||||||
/>);
|
/>);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user