Signed-off-by: falkTX <falktx@falktx.com>web-ui
@@ -21,6 +21,7 @@ | |||
// -------------------------------------------------------------------------------------------------------------------- | |||
// TODO private data | |||
START_NAMESPACE_DISTRHO | |||
struct WebViewData; | |||
@@ -53,12 +54,16 @@ public: | |||
void reload(); | |||
protected: | |||
virtual void onMessage(char* message); | |||
void onResize(const ResizeEvent& ev) override; | |||
private: | |||
const DISTRHO_NAMESPACE::WebViewHandle webview; | |||
void onDisplay() override {} | |||
// TODO inside private data | |||
static void _on_msg(void*, char*); | |||
DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(WebViewWidget) | |||
}; | |||
@@ -33,10 +33,9 @@ WebViewWidget::WebViewWidget(Window& windowToMapTo) | |||
webview(webViewCreate(windowToMapTo.getNativeWindowHandle(), | |||
windowToMapTo.getWidth(), | |||
windowToMapTo.getHeight(), | |||
windowToMapTo.getScaleFactor())) | |||
windowToMapTo.getScaleFactor(), | |||
WebViewOptions(_on_msg, this))) | |||
{ | |||
// FIXME wait till process is here? | |||
d_msleep(200); | |||
} | |||
WebViewWidget::~WebViewWidget() | |||
@@ -57,6 +56,15 @@ void WebViewWidget::reload() | |||
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) | |||
{ | |||
TopLevelWidget::onResize(ev); | |||
@@ -371,6 +371,13 @@ protected: | |||
void onResize(const ResizeEvent& ev) override; | |||
#endif | |||
/* -------------------------------------------------------------------------------------------------------- | |||
* WebView message handling, internal */ | |||
#if DISTRHO_UI_WEB_VIEW | |||
void onMessage(char* message) override; | |||
#endif | |||
// ------------------------------------------------------------------------------------------------------- | |||
private: | |||
@@ -82,13 +82,15 @@ | |||
#define WEB_VIEW_DELEGATE_CLASS_NAME \ | |||
MACRO_NAME(WebViewDelegate_, _, DISTRHO_NAMESPACE) | |||
// FIXME | |||
static bool loaded = false; | |||
@interface WEB_VIEW_DELEGATE_CLASS_NAME : NSObject<WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate> | |||
@end | |||
@implementation WEB_VIEW_DELEGATE_CLASS_NAME | |||
@implementation WEB_VIEW_DELEGATE_CLASS_NAME { | |||
@public | |||
WebViewMessageCallback callback; | |||
void* callbackPtr; | |||
bool loaded; | |||
} | |||
- (void)webView:(WKWebView *)webview | |||
didFinishNavigation:(WKNavigation*)navigation | |||
@@ -195,9 +197,13 @@ static bool loaded = false; | |||
didReceiveScriptMessage:(WKScriptMessage*)message | |||
{ | |||
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 | |||
@@ -216,6 +222,7 @@ START_NAMESPACE_DISTRHO | |||
// ----------------------------------------------------------------------------------------------------------- | |||
struct WebViewData { | |||
const WebViewMessageCallback callback; | |||
#if WEB_VIEW_USING_CHOC | |||
choc::ui::WebView* const webview; | |||
#elif WEB_VIEW_USING_MACOS_WEBKIT | |||
@@ -358,7 +365,7 @@ WebViewHandle webViewCreate(const uintptr_t windowId, | |||
ShowWindow(hwnd, SW_SHOW); | |||
#endif | |||
return new WebViewData{webview.release()}; | |||
return new WebViewData{options.callback, webview.release()}; | |||
#elif WEB_VIEW_USING_MACOS_WEBKIT | |||
NSView* const view = reinterpret_cast<NSView*>(windowId); | |||
@@ -389,6 +396,10 @@ WebViewHandle webViewCreate(const uintptr_t windowId, | |||
// TODO webkit_web_view_set_background_color | |||
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.UIDelegate = delegate; | |||
@@ -410,13 +421,13 @@ WebViewHandle webViewCreate(const uintptr_t windowId, | |||
d_stdout("waiting for load"); | |||
if (! loaded) | |||
if (! delegate->loaded) | |||
{ | |||
NSAutoreleasePool* const pool = [[NSAutoreleasePool alloc] init]; | |||
NSDate* const date = [NSDate distantPast]; | |||
NSDate* const date = [NSDate distantFuture]; | |||
NSEvent* event; | |||
while (! loaded) | |||
while (! delegate->loaded) | |||
{ | |||
event = [NSApp | |||
#ifdef __MAC_10_12 | |||
@@ -437,13 +448,15 @@ WebViewHandle webViewCreate(const uintptr_t windowId, | |||
[pool release]; | |||
} | |||
d_stdout("waiting done"); | |||
[webview setHidden:NO]; | |||
[nsurl release]; | |||
[config 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 | |||
char ldlinux[PATH_MAX] = {}; | |||
getFilenameFromFunctionPtr(ldlinux, dlsym(nullptr, "_rtld_global")); | |||
@@ -482,10 +495,7 @@ WebViewHandle webViewCreate(const uintptr_t windowId, | |||
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 }; | |||
handle->p.start(args, envp); | |||
@@ -23,6 +23,7 @@ | |||
struct WebViewData; | |||
typedef WebViewData* WebViewHandle; | |||
typedef void (*WebViewMessageCallback)(void* arg, char* msg); | |||
// -------------------------------------------------------------------------------------------------------------------- | |||
@@ -44,9 +45,23 @@ struct WebViewOptions { | |||
PositionOffset() : x(0), y(0) {} | |||
} offset; | |||
/** | |||
Message callback triggered from JavaScript code inside the WebView. | |||
*/ | |||
WebViewMessageCallback callback; | |||
void* callbackPtr; | |||
/** Constructor for default values */ | |||
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 | |||
(void)automaticallyScaleAndSetAsMinimumSize; | |||
#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() | |||
@@ -381,37 +393,57 @@ uintptr_t UI::getNextWindowId() noexcept | |||
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 | |||
(void)index; | |||
(void)value; | |||
#endif | |||
#endif | |||
} | |||
#if DISTRHO_PLUGIN_WANT_PROGRAMS | |||
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 | |||
(void)index; | |||
#endif | |||
#endif | |||
} | |||
#endif | |||
#if DISTRHO_PLUGIN_WANT_STATE | |||
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 | |||
(void)key; | |||
(void)value; | |||
#endif | |||
#endif | |||
} | |||
#endif | |||
@@ -420,12 +452,18 @@ void UI::stateChanged(const char* const key, const char* const value) | |||
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 | |||
(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 |
@@ -237,8 +237,7 @@ protected: | |||
{ | |||
fOutLeft = tmpLeft; | |||
fOutRight = tmpRight; | |||
// TODO | |||
// fNeedsReset = false; | |||
fNeedsReset = false; | |||
} | |||
else | |||
{ | |||
@@ -24,41 +24,10 @@ class ExampleUIMeters : public UI | |||
{ | |||
public: | |||
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: | |||
/** | |||
Set our UI class as non-copyable and add a leak detector just in case. | |||
@@ -14,6 +14,20 @@ | |||
setTimeout(function() { | |||
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) | |||
function repaint() { | |||
@@ -21,9 +35,12 @@ | |||
const rmeter = document.getElementById('right-meter-x'); | |||
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) + '%'); | |||
setTimeout(function() { | |||
setState('reset', ''); | |||
}, 1) | |||
} | |||
function updateColor(color) { | |||
if (fColorValue === color) | |||
function updateColor(color, forced) { | |||
if (fColorValue === color && !forced) | |||
return; | |||
fColorValue = color; | |||