Signed-off-by: falkTX <falktx@falktx.com>web-ui
@@ -21,6 +21,7 @@ | |||||
// -------------------------------------------------------------------------------------------------------------------- | // -------------------------------------------------------------------------------------------------------------------- | ||||
// TODO private data | |||||
START_NAMESPACE_DISTRHO | START_NAMESPACE_DISTRHO | ||||
struct WebViewData; | struct WebViewData; | ||||
@@ -53,12 +54,16 @@ public: | |||||
void reload(); | void reload(); | ||||
protected: | protected: | ||||
virtual void onMessage(char* message); | |||||
void onResize(const ResizeEvent& ev) override; | void onResize(const ResizeEvent& ev) override; | ||||
private: | private: | ||||
const DISTRHO_NAMESPACE::WebViewHandle webview; | const DISTRHO_NAMESPACE::WebViewHandle webview; | ||||
void onDisplay() override {} | void onDisplay() override {} | ||||
// TODO inside private data | |||||
static void _on_msg(void*, char*); | |||||
DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WebViewWidget) | DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WebViewWidget) | ||||
}; | }; | ||||
@@ -33,10 +33,9 @@ WebViewWidget::WebViewWidget(Window& windowToMapTo) | |||||
webview(webViewCreate(windowToMapTo.getNativeWindowHandle(), | webview(webViewCreate(windowToMapTo.getNativeWindowHandle(), | ||||
windowToMapTo.getWidth(), | windowToMapTo.getWidth(), | ||||
windowToMapTo.getHeight(), | windowToMapTo.getHeight(), | ||||
windowToMapTo.getScaleFactor())) | |||||
windowToMapTo.getScaleFactor(), | |||||
WebViewOptions(_on_msg, this))) | |||||
{ | { | ||||
// FIXME wait till process is here? | |||||
d_msleep(200); | |||||
} | } | ||||
WebViewWidget::~WebViewWidget() | WebViewWidget::~WebViewWidget() | ||||
@@ -57,6 +56,15 @@ void WebViewWidget::reload() | |||||
webViewReload(webview); | webViewReload(webview); | ||||
} | } | ||||
void WebViewWidget::_on_msg(void* const arg, char* const message) | |||||
{ | |||||
static_cast<WebViewWidget*>(arg)->onMessage(message); | |||||
} | |||||
void WebViewWidget::onMessage(char*) | |||||
{ | |||||
} | |||||
void WebViewWidget::onResize(const ResizeEvent& ev) | void WebViewWidget::onResize(const ResizeEvent& ev) | ||||
{ | { | ||||
TopLevelWidget::onResize(ev); | TopLevelWidget::onResize(ev); | ||||
@@ -371,6 +371,13 @@ protected: | |||||
void onResize(const ResizeEvent& ev) override; | void onResize(const ResizeEvent& ev) override; | ||||
#endif | #endif | ||||
/* -------------------------------------------------------------------------------------------------------- | |||||
* WebView message handling, internal */ | |||||
#if DISTRHO_UI_WEB_VIEW | |||||
void onMessage(char* message) override; | |||||
#endif | |||||
// ------------------------------------------------------------------------------------------------------- | // ------------------------------------------------------------------------------------------------------- | ||||
private: | private: | ||||
@@ -82,13 +82,15 @@ | |||||
#define WEB_VIEW_DELEGATE_CLASS_NAME \ | #define WEB_VIEW_DELEGATE_CLASS_NAME \ | ||||
MACRO_NAME(WebViewDelegate_, _, DISTRHO_NAMESPACE) | MACRO_NAME(WebViewDelegate_, _, DISTRHO_NAMESPACE) | ||||
// FIXME | |||||
static bool loaded = false; | |||||
@interface WEB_VIEW_DELEGATE_CLASS_NAME : NSObject<WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate> | @interface WEB_VIEW_DELEGATE_CLASS_NAME : NSObject<WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate> | ||||
@end | @end | ||||
@implementation WEB_VIEW_DELEGATE_CLASS_NAME | |||||
@implementation WEB_VIEW_DELEGATE_CLASS_NAME { | |||||
@public | |||||
WebViewMessageCallback callback; | |||||
void* callbackPtr; | |||||
bool loaded; | |||||
} | |||||
- (void)webView:(WKWebView *)webview | - (void)webView:(WKWebView *)webview | ||||
didFinishNavigation:(WKNavigation*)navigation | didFinishNavigation:(WKNavigation*)navigation | ||||
@@ -195,9 +197,13 @@ static bool loaded = false; | |||||
didReceiveScriptMessage:(WKScriptMessage*)message | didReceiveScriptMessage:(WKScriptMessage*)message | ||||
{ | { | ||||
NSString* const nsstring = static_cast<NSString*>([message body]); | NSString* const nsstring = static_cast<NSString*>([message body]); | ||||
const char* const string = [nsstring UTF8String]; | |||||
char* const string = strdup([nsstring UTF8String]); | |||||
d_debug("JS call received '%s' %p", string, callback); | |||||
if (callback != nullptr) | |||||
callback(callbackPtr, string); | |||||
d_stdout("JS call received '%s'", string); | |||||
std::free(string); | |||||
} | } | ||||
@end | @end | ||||
@@ -216,6 +222,7 @@ START_NAMESPACE_DISTRHO | |||||
// ----------------------------------------------------------------------------------------------------------- | // ----------------------------------------------------------------------------------------------------------- | ||||
struct WebViewData { | struct WebViewData { | ||||
const WebViewMessageCallback callback; | |||||
#if WEB_VIEW_USING_CHOC | #if WEB_VIEW_USING_CHOC | ||||
choc::ui::WebView* const webview; | choc::ui::WebView* const webview; | ||||
#elif WEB_VIEW_USING_MACOS_WEBKIT | #elif WEB_VIEW_USING_MACOS_WEBKIT | ||||
@@ -358,7 +365,7 @@ WebViewHandle webViewCreate(const uintptr_t windowId, | |||||
ShowWindow(hwnd, SW_SHOW); | ShowWindow(hwnd, SW_SHOW); | ||||
#endif | #endif | ||||
return new WebViewData{webview.release()}; | |||||
return new WebViewData{options.callback, webview.release()}; | |||||
#elif WEB_VIEW_USING_MACOS_WEBKIT | #elif WEB_VIEW_USING_MACOS_WEBKIT | ||||
NSView* const view = reinterpret_cast<NSView*>(windowId); | NSView* const view = reinterpret_cast<NSView*>(windowId); | ||||
@@ -389,6 +396,10 @@ WebViewHandle webViewCreate(const uintptr_t windowId, | |||||
// TODO webkit_web_view_set_background_color | // TODO webkit_web_view_set_background_color | ||||
WEB_VIEW_DELEGATE_CLASS_NAME* const delegate = [[WEB_VIEW_DELEGATE_CLASS_NAME alloc] init]; | WEB_VIEW_DELEGATE_CLASS_NAME* const delegate = [[WEB_VIEW_DELEGATE_CLASS_NAME alloc] init]; | ||||
delegate->callback = options.callback; | |||||
delegate->callbackPtr = options.callbackPtr; | |||||
delegate->loaded = false; | |||||
webview.navigationDelegate = delegate; | webview.navigationDelegate = delegate; | ||||
webview.UIDelegate = delegate; | webview.UIDelegate = delegate; | ||||
@@ -410,13 +421,13 @@ WebViewHandle webViewCreate(const uintptr_t windowId, | |||||
d_stdout("waiting for load"); | d_stdout("waiting for load"); | ||||
if (! loaded) | |||||
if (! delegate->loaded) | |||||
{ | { | ||||
NSAutoreleasePool* const pool = [[NSAutoreleasePool alloc] init]; | NSAutoreleasePool* const pool = [[NSAutoreleasePool alloc] init]; | ||||
NSDate* const date = [NSDate distantPast]; | |||||
NSDate* const date = [NSDate distantFuture]; | |||||
NSEvent* event; | NSEvent* event; | ||||
while (! loaded) | |||||
while (! delegate->loaded) | |||||
{ | { | ||||
event = [NSApp | event = [NSApp | ||||
#ifdef __MAC_10_12 | #ifdef __MAC_10_12 | ||||
@@ -437,13 +448,15 @@ WebViewHandle webViewCreate(const uintptr_t windowId, | |||||
[pool release]; | [pool release]; | ||||
} | } | ||||
d_stdout("waiting done"); | |||||
[webview setHidden:NO]; | [webview setHidden:NO]; | ||||
[nsurl release]; | [nsurl release]; | ||||
[config release]; | [config release]; | ||||
[prefs release]; | [prefs release]; | ||||
return new WebViewData{view, webview, urlreq, delegate}; | |||||
return new WebViewData{options.callback, view, webview, urlreq, delegate}; | |||||
#elif WEB_VIEW_USING_X11_IPC | #elif WEB_VIEW_USING_X11_IPC | ||||
char ldlinux[PATH_MAX] = {}; | char ldlinux[PATH_MAX] = {}; | ||||
getFilenameFromFunctionPtr(ldlinux, dlsym(nullptr, "_rtld_global")); | getFilenameFromFunctionPtr(ldlinux, dlsym(nullptr, "_rtld_global")); | ||||
@@ -482,10 +495,7 @@ WebViewHandle webViewCreate(const uintptr_t windowId, | |||||
envp[e++] = nullptr; | envp[e++] = nullptr; | ||||
} | } | ||||
WebViewData* const handle = new WebViewData(); | |||||
handle->display = display; | |||||
handle->childWindow = 0; | |||||
handle->ourWindow = windowId; | |||||
WebViewData* const handle = new WebViewData{options.callback, {}, display, 0, windowId}; | |||||
const char* const args[] = { ldlinux, filename, "dpf-ld-linux-webview", nullptr }; | const char* const args[] = { ldlinux, filename, "dpf-ld-linux-webview", nullptr }; | ||||
handle->p.start(args, envp); | handle->p.start(args, envp); | ||||
@@ -23,6 +23,7 @@ | |||||
struct WebViewData; | struct WebViewData; | ||||
typedef WebViewData* WebViewHandle; | typedef WebViewData* WebViewHandle; | ||||
typedef void (*WebViewMessageCallback)(void* arg, char* msg); | |||||
// -------------------------------------------------------------------------------------------------------------------- | // -------------------------------------------------------------------------------------------------------------------- | ||||
@@ -44,9 +45,23 @@ struct WebViewOptions { | |||||
PositionOffset() : x(0), y(0) {} | PositionOffset() : x(0), y(0) {} | ||||
} offset; | } offset; | ||||
/** | |||||
Message callback triggered from JavaScript code inside the WebView. | |||||
*/ | |||||
WebViewMessageCallback callback; | |||||
void* callbackPtr; | |||||
/** Constructor for default values */ | /** Constructor for default values */ | ||||
WebViewOptions() | WebViewOptions() | ||||
: offset() {} | |||||
: offset(), | |||||
callback(nullptr), | |||||
callbackPtr(nullptr) {} | |||||
/** Constructor providing a callback */ | |||||
WebViewOptions(const WebViewMessageCallback cb, void* const ptr) | |||||
: offset(), | |||||
callback(cb), | |||||
callbackPtr(ptr) {} | |||||
}; | }; | ||||
// -------------------------------------------------------------------------------------------------------------------- | // -------------------------------------------------------------------------------------------------------------------- | ||||
@@ -264,6 +264,18 @@ UI::UI(const uint width, const uint height, const bool automaticallyScaleAndSetA | |||||
// unused | // unused | ||||
(void)automaticallyScaleAndSetAsMinimumSize; | (void)automaticallyScaleAndSetAsMinimumSize; | ||||
#endif | #endif | ||||
#if DISTRHO_UI_WEB_VIEW | |||||
evaluateJS("\ | |||||
function editParameter(index, started){ window.webkit.messageHandlers.external.postMessage('editparam ' + index + ' ' + (started ? 1 : 0)) }\ | |||||
function setParameterValue(index, value){ window.webkit.messageHandlers.external.postMessage('setparam ' + index + ' ' + value) }\ | |||||
"); | |||||
#if DISTRHO_PLUGIN_WANT_STATE | |||||
evaluateJS("\ | |||||
function setState(key, value){ window.webkit.messageHandlers.external.postMessage('setstate ' + key + ' ' + value) }\ | |||||
"); | |||||
#endif | |||||
#endif | |||||
} | } | ||||
UI::~UI() | UI::~UI() | ||||
@@ -381,37 +393,57 @@ uintptr_t UI::getNextWindowId() noexcept | |||||
void UI::parameterChanged(const uint32_t index, const float value) | void UI::parameterChanged(const uint32_t index, const float value) | ||||
{ | { | ||||
#if DISTRHO_UI_WEB_VIEW | |||||
evaluateJS("typeof(parameterChanged) === 'function' && parameterChanged(0, 0);"); | |||||
#else | |||||
#if DISTRHO_UI_WEB_VIEW | |||||
char msg[128]; | |||||
{ | |||||
const ScopedSafeLocale ssl; | |||||
std::snprintf(msg, sizeof(msg) - 1, | |||||
"typeof(parameterChanged) === 'function' && parameterChanged(%u,%f)", index, value); | |||||
} | |||||
evaluateJS(msg); | |||||
#else | |||||
// unused | // unused | ||||
(void)index; | (void)index; | ||||
(void)value; | (void)value; | ||||
#endif | |||||
#endif | |||||
} | } | ||||
#if DISTRHO_PLUGIN_WANT_PROGRAMS | #if DISTRHO_PLUGIN_WANT_PROGRAMS | ||||
void UI::programLoaded(const uint32_t index) | void UI::programLoaded(const uint32_t index) | ||||
{ | { | ||||
#if DISTRHO_UI_WEB_VIEW | |||||
evaluateJS("typeof(programLoaded) === 'function' && programLoaded(0);"); | |||||
#else | |||||
#if DISTRHO_UI_WEB_VIEW | |||||
char msg[128]; | |||||
std::snprintf(msg, sizeof(msg) - 1, | |||||
"typeof(programLoaded) === 'function' && programLoaded(%u)", index); | |||||
evaluateJS(msg); | |||||
#else | |||||
// unused | // unused | ||||
(void)index; | (void)index; | ||||
#endif | |||||
#endif | |||||
} | } | ||||
#endif | #endif | ||||
#if DISTRHO_PLUGIN_WANT_STATE | #if DISTRHO_PLUGIN_WANT_STATE | ||||
void UI::stateChanged(const char* const key, const char* const value) | void UI::stateChanged(const char* const key, const char* const value) | ||||
{ | { | ||||
#if DISTRHO_UI_WEB_VIEW | |||||
evaluateJS("typeof(stateChanged) === 'function' && stateChanged('', '');"); | |||||
#else | |||||
#if DISTRHO_UI_WEB_VIEW | |||||
const size_t keylen = std::strlen(key); | |||||
const size_t valuelen = std::strlen(value); | |||||
const size_t msglen = keylen + valuelen + 60; | |||||
if (char* const msg = static_cast<char*>(std::malloc(msglen))) | |||||
{ | |||||
// TODO escape \\' | |||||
std::snprintf(msg, sizeof(msglen) - 1, | |||||
"typeof(stateChanged) === 'function' && stateChanged('%s','%s')", key, value); | |||||
msg[msglen - 1] = '\0'; | |||||
evaluateJS(msg); | |||||
std::free(msg); | |||||
} | |||||
#else | |||||
// unused | // unused | ||||
(void)key; | (void)key; | ||||
(void)value; | (void)value; | ||||
#endif | |||||
#endif | |||||
} | } | ||||
#endif | #endif | ||||
@@ -420,12 +452,18 @@ void UI::stateChanged(const char* const key, const char* const value) | |||||
void UI::sampleRateChanged(const double sampleRate) | void UI::sampleRateChanged(const double sampleRate) | ||||
{ | { | ||||
#if DISTRHO_UI_WEB_VIEW | |||||
evaluateJS("typeof(sampleRateChanged) === 'function' && sampleRateChanged(0);"); | |||||
#else | |||||
#if DISTRHO_UI_WEB_VIEW | |||||
char msg[128]; | |||||
{ | |||||
const ScopedSafeLocale ssl; | |||||
std::snprintf(msg, sizeof(msg) - 1, | |||||
"typeof(sampleRateChanged) === 'function' && sampleRateChanged(%f)", sampleRate); | |||||
} | |||||
evaluateJS(msg); | |||||
#else | |||||
// unused | // unused | ||||
(void)sampleRate; | (void)sampleRate; | ||||
#endif | |||||
#endif | |||||
} | } | ||||
/* ------------------------------------------------------------------------------------------------------------ | /* ------------------------------------------------------------------------------------------------------------ | ||||
@@ -515,4 +553,59 @@ void UI::requestSizeChange(const uint width, const uint height) | |||||
// ----------------------------------------------------------------------------------------------------------- | // ----------------------------------------------------------------------------------------------------------- | ||||
#if DISTRHO_UI_WEB_VIEW | |||||
void UI::onMessage(char* const message) | |||||
{ | |||||
if (std::strncmp(message, "setparam ", 9) == 0) | |||||
{ | |||||
char* const strindex = message + 9; | |||||
char* const sep = std::strchr(strindex, ' '); | |||||
DISTRHO_SAFE_ASSERT_RETURN(sep != nullptr,); | |||||
*sep = 0; | |||||
char* const strvalue = sep + 1; | |||||
const uint32_t index = std::atoi(strindex); | |||||
float value; | |||||
{ | |||||
const ScopedSafeLocale ssl; | |||||
value = std::atof(strvalue); | |||||
} | |||||
setParameterValue(index, value); | |||||
return; | |||||
} | |||||
if (std::strncmp(message, "editparam ", 10) == 0) | |||||
{ | |||||
char* const strindex = message + 10; | |||||
char* const sep = std::strchr(strindex, ' '); | |||||
DISTRHO_SAFE_ASSERT_RETURN(sep != nullptr,); | |||||
*sep = 0; | |||||
char* const strstarted = sep + 1; | |||||
const uint32_t index = std::atoi(strindex); | |||||
const bool started = std::atoi(strstarted) != 0; | |||||
editParameter(index, started); | |||||
return; | |||||
} | |||||
#if DISTRHO_PLUGIN_WANT_STATE | |||||
if (std::strncmp(message, "setstate ", 9) == 0) | |||||
{ | |||||
char* const key = message + 9; | |||||
char* const sep = std::strchr(key, ' '); | |||||
DISTRHO_SAFE_ASSERT_RETURN(sep != nullptr,); | |||||
*sep = 0; | |||||
char* const value = sep + 1; | |||||
setState(key, value); | |||||
return; | |||||
} | |||||
#endif | |||||
d_stderr("UI received unknown message %s", message); | |||||
} | |||||
#endif | |||||
// ----------------------------------------------------------------------------------------------------------- | |||||
END_NAMESPACE_DISTRHO | END_NAMESPACE_DISTRHO |
@@ -237,8 +237,7 @@ protected: | |||||
{ | { | ||||
fOutLeft = tmpLeft; | fOutLeft = tmpLeft; | ||||
fOutRight = tmpRight; | fOutRight = tmpRight; | ||||
// TODO | |||||
// fNeedsReset = false; | |||||
fNeedsReset = false; | |||||
} | } | ||||
else | else | ||||
{ | { | ||||
@@ -24,41 +24,10 @@ class ExampleUIMeters : public UI | |||||
{ | { | ||||
public: | public: | ||||
ExampleUIMeters() | ExampleUIMeters() | ||||
: UI(600, 400, true) | |||||
: UI(100, 500, true) | |||||
{ | { | ||||
} | } | ||||
protected: | |||||
/* -------------------------------------------------------------------------------------------------------- | |||||
* DSP/Plugin Callbacks */ | |||||
/** | |||||
A parameter has changed on the plugin side. | |||||
This is called by the host to inform the UI about parameter changes. | |||||
*/ | |||||
void parameterChanged(uint32_t index, float value) override | |||||
{ | |||||
// d_stdout("param changed %u %f", index, value); | |||||
char msg[512]; | |||||
{ | |||||
const ScopedSafeLocale ssl; | |||||
std::snprintf(msg, sizeof(msg) - 1, | |||||
"typeof(parameterChanged) === 'function' && parameterChanged(%u, %f)", index, value); | |||||
} | |||||
evaluateJS(msg); | |||||
} | |||||
/** | |||||
A state has changed on the plugin side. | |||||
This is called by the host to inform the UI about state changes. | |||||
*/ | |||||
void stateChanged(const char*, const char*) override | |||||
{ | |||||
// nothing here | |||||
} | |||||
// ------------------------------------------------------------------------------------------------------- | |||||
private: | private: | ||||
/** | /** | ||||
Set our UI class as non-copyable and add a leak detector just in case. | Set our UI class as non-copyable and add a leak detector just in case. | ||||
@@ -14,6 +14,20 @@ | |||||
setTimeout(function() { | setTimeout(function() { | ||||
document.getElementById('user-agent').textContent = window.navigator.userAgent; | document.getElementById('user-agent').textContent = window.navigator.userAgent; | ||||
document.getElementById('left-meter').onclick = function() { | |||||
console.log("left meter clicked"); | |||||
fColorValue = fColorValue == 1 ? 0 : 1; | |||||
updateColor(fColorValue, true); | |||||
setParameterValue(0, fColorValue); | |||||
repaint(); | |||||
} | |||||
document.getElementById('right-meter').onclick = function() { | |||||
console.log("right meter clicked"); | |||||
fColorValue = fColorValue == 1 ? 0 : 1; | |||||
updateColor(fColorValue, true); | |||||
setParameterValue(0, fColorValue); | |||||
repaint(); | |||||
} | |||||
}, 1) | }, 1) | ||||
function repaint() { | function repaint() { | ||||
@@ -21,9 +35,12 @@ | |||||
const rmeter = document.getElementById('right-meter-x'); | const rmeter = document.getElementById('right-meter-x'); | ||||
lmeter.setAttribute('style', 'background:' + fColor + ';top:' + (100 * (1.0 - fOutLeft)) + '%;height:' + (100 * fOutLeft) + '%'); | lmeter.setAttribute('style', 'background:' + fColor + ';top:' + (100 * (1.0 - fOutLeft)) + '%;height:' + (100 * fOutLeft) + '%'); | ||||
rmeter.setAttribute('style', 'background:' + fColor + ';top:' + (100 * (1.0 - fOutRight)) + '%;height:' + (100 * fOutRight) + '%'); | rmeter.setAttribute('style', 'background:' + fColor + ';top:' + (100 * (1.0 - fOutRight)) + '%;height:' + (100 * fOutRight) + '%'); | ||||
setTimeout(function() { | |||||
setState('reset', ''); | |||||
}, 1) | |||||
} | } | ||||
function updateColor(color) { | |||||
if (fColorValue === color) | |||||
function updateColor(color, forced) { | |||||
if (fColorValue === color && !forced) | |||||
return; | return; | ||||
fColorValue = color; | fColorValue = color; | ||||