First Commit
This commit is contained in:
+356
@@ -0,0 +1,356 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
package com.rmsl.juce;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.http.SslError;
|
||||
import android.os.Build;
|
||||
import android.os.Message;
|
||||
import android.webkit.JavascriptInterface;
|
||||
import android.webkit.ValueCallback;
|
||||
import android.webkit.WebResourceResponse;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.webkit.SslErrorHandler;
|
||||
import android.webkit.WebChromeClient;
|
||||
|
||||
import java.lang.annotation.Native;
|
||||
import java.util.ArrayList;
|
||||
|
||||
//==============================================================================
|
||||
public class JuceWebViewClasses
|
||||
{
|
||||
static public class NativeInterface
|
||||
{
|
||||
private long host;
|
||||
private final Object hostLock = new Object ();
|
||||
|
||||
//==============================================================================
|
||||
public NativeInterface (long hostToUse)
|
||||
{
|
||||
host = hostToUse;
|
||||
}
|
||||
|
||||
public void hostDeleted ()
|
||||
{
|
||||
synchronized (hostLock)
|
||||
{
|
||||
host = 0;
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
public void onPageStarted (WebView view, String url)
|
||||
{
|
||||
if (host == 0)
|
||||
return;
|
||||
|
||||
handleResourceRequest (host, view, url);
|
||||
}
|
||||
|
||||
public WebResourceResponse shouldInterceptRequest (WebView view, WebResourceRequest request)
|
||||
{
|
||||
synchronized (hostLock)
|
||||
{
|
||||
if (host != 0)
|
||||
{
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
|
||||
return handleResourceRequest (host, view, request.getUrl().toString());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void onPageFinished (WebView view, String url)
|
||||
{
|
||||
if (host == 0)
|
||||
return;
|
||||
|
||||
webViewPageLoadFinished (host, view, url);
|
||||
}
|
||||
|
||||
public void onReceivedSslError (WebView view, SslErrorHandler handler, SslError error)
|
||||
{
|
||||
if (host == 0)
|
||||
return;
|
||||
|
||||
webViewReceivedSslError (host, view, handler, error);
|
||||
}
|
||||
|
||||
public void onReceivedHttpError (WebView view, WebResourceRequest request, WebResourceResponse errorResponse)
|
||||
{
|
||||
if (host == 0)
|
||||
return;
|
||||
|
||||
webViewReceivedHttpError (host, view, request, errorResponse);
|
||||
}
|
||||
|
||||
public void onCloseWindow (WebView window)
|
||||
{
|
||||
if (host == 0)
|
||||
return;
|
||||
|
||||
webViewCloseWindowRequest (host, window);
|
||||
}
|
||||
|
||||
public boolean onCreateWindow (WebView view, boolean isDialog,
|
||||
boolean isUserGesture, Message resultMsg)
|
||||
{
|
||||
if (host == 0)
|
||||
return false;
|
||||
|
||||
webViewCreateWindowRequest (host, view);
|
||||
return false;
|
||||
}
|
||||
|
||||
public String postMessageHandler (String message)
|
||||
{
|
||||
synchronized (hostLock)
|
||||
{
|
||||
if (host != 0)
|
||||
{
|
||||
String result = postMessage (host, message);
|
||||
|
||||
if (result == null)
|
||||
return "";
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
public void handleJavascriptEvaluationResult (long evalId, String result)
|
||||
{
|
||||
if (host == 0)
|
||||
return;
|
||||
|
||||
evaluationResultHandler (host, evalId, result);
|
||||
}
|
||||
|
||||
public boolean shouldOverrideUrlLoading (String url)
|
||||
{
|
||||
if (host == 0)
|
||||
return false;
|
||||
|
||||
return ! pageAboutToLoad (host, url);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
private native WebResourceResponse handleResourceRequest (long host, WebView view, String url);
|
||||
private native void webViewPageLoadFinished (long host, WebView view, String url);
|
||||
private native void webViewReceivedSslError (long host, WebView view, SslErrorHandler handler, SslError error);
|
||||
private native void webViewReceivedHttpError (long host, WebView view, WebResourceRequest request, WebResourceResponse errorResponse);
|
||||
|
||||
private native void webViewCloseWindowRequest (long host, WebView view);
|
||||
private native void webViewCreateWindowRequest (long host, WebView view);
|
||||
|
||||
private native void evaluationResultHandler (long host, long evalId, String result);
|
||||
private native String postMessage (long host, String message);
|
||||
|
||||
private native boolean pageAboutToLoad (long host, String url);
|
||||
}
|
||||
|
||||
static public class Client extends WebViewClient
|
||||
{
|
||||
private NativeInterface nativeInterface;
|
||||
|
||||
//==============================================================================
|
||||
public Client (NativeInterface nativeInterfaceIn)
|
||||
{
|
||||
nativeInterface = nativeInterfaceIn;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
@Override
|
||||
public void onPageFinished (WebView view, String url)
|
||||
{
|
||||
nativeInterface.onPageFinished (view, url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedSslError (WebView view, SslErrorHandler handler, SslError error)
|
||||
{
|
||||
nativeInterface.onReceivedSslError (view, handler, error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedHttpError (WebView view, WebResourceRequest request, WebResourceResponse errorResponse)
|
||||
{
|
||||
nativeInterface.onReceivedHttpError (view, request, errorResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageStarted (WebView view, String url, Bitmap favicon)
|
||||
{
|
||||
nativeInterface.onPageStarted (view, url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebResourceResponse shouldInterceptRequest (WebView view, WebResourceRequest request)
|
||||
{
|
||||
return nativeInterface.shouldInterceptRequest (view, request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading (WebView view, WebResourceRequest request)
|
||||
{
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
|
||||
return nativeInterface.shouldOverrideUrlLoading (request.getUrl().toString());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading (WebView view, String url)
|
||||
{
|
||||
return nativeInterface.shouldOverrideUrlLoading (url);
|
||||
}
|
||||
}
|
||||
|
||||
static public class ChromeClient extends WebChromeClient
|
||||
{
|
||||
private NativeInterface nativeInterface;
|
||||
|
||||
//==============================================================================
|
||||
public ChromeClient (NativeInterface nativeInterfaceIn)
|
||||
{
|
||||
nativeInterface = nativeInterfaceIn;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
@Override
|
||||
public void onCloseWindow (WebView window)
|
||||
{
|
||||
nativeInterface.onCloseWindow (window);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateWindow (WebView view, boolean isDialog,
|
||||
boolean isUserGesture, Message resultMsg)
|
||||
{
|
||||
return nativeInterface.onCreateWindow (view, isDialog, isUserGesture, resultMsg);
|
||||
}
|
||||
}
|
||||
|
||||
static public class WebAppInterface
|
||||
{
|
||||
private NativeInterface nativeInterface;
|
||||
String userScripts;
|
||||
|
||||
//==============================================================================
|
||||
public WebAppInterface (NativeInterface nativeInterfaceIn, String userScriptsIn)
|
||||
{
|
||||
nativeInterface = nativeInterfaceIn;
|
||||
userScripts = userScriptsIn;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
@JavascriptInterface
|
||||
public String postMessage (String message)
|
||||
{
|
||||
return nativeInterface.postMessageHandler (message);
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public String getAndroidUserScripts()
|
||||
{
|
||||
return userScripts;
|
||||
}
|
||||
}
|
||||
|
||||
static public class JuceWebView extends WebView
|
||||
{
|
||||
private NativeInterface nativeInterface;
|
||||
private long evaluationId = 0;
|
||||
|
||||
public JuceWebView (Context context, long host, String userAgent, String initScripts)
|
||||
{
|
||||
super (context);
|
||||
|
||||
nativeInterface = new NativeInterface (host);
|
||||
|
||||
WebSettings settings = getSettings();
|
||||
settings.setJavaScriptEnabled (true);
|
||||
settings.setBuiltInZoomControls (true);
|
||||
settings.setDisplayZoomControls (false);
|
||||
settings.setSupportMultipleWindows (true);
|
||||
settings.setUserAgentString (userAgent);
|
||||
|
||||
setWebChromeClient (new ChromeClient (nativeInterface));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
|
||||
{
|
||||
setWebContentsDebuggingEnabled (true);
|
||||
}
|
||||
|
||||
setWebViewClient (new Client (nativeInterface));
|
||||
addJavascriptInterface (new WebAppInterface (nativeInterface, initScripts), "__JUCE__");
|
||||
}
|
||||
|
||||
public void disconnectNative()
|
||||
{
|
||||
nativeInterface.hostDeleted();
|
||||
}
|
||||
|
||||
public long evaluateJavascript (String script)
|
||||
{
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
|
||||
{
|
||||
final long currentEvaluationId = evaluationId++;
|
||||
final NativeInterface accessibleNativeInterface = nativeInterface;
|
||||
|
||||
super.evaluateJavascript (script, new ValueCallback<String>()
|
||||
{
|
||||
@Override
|
||||
public void onReceiveValue(String result)
|
||||
{
|
||||
accessibleNativeInterface.handleJavascriptEvaluationResult (currentEvaluationId,
|
||||
result);
|
||||
}
|
||||
});
|
||||
|
||||
return currentEvaluationId;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
package com.rmsl.juce;
|
||||
|
||||
import com.google.firebase.iid.*;
|
||||
|
||||
public final class JuceFirebaseInstanceIdService extends FirebaseInstanceIdService
|
||||
{
|
||||
private native void firebaseInstanceIdTokenRefreshed (String token);
|
||||
|
||||
@Override
|
||||
public void onTokenRefresh()
|
||||
{
|
||||
String token = FirebaseInstanceId.getInstance().getToken();
|
||||
|
||||
firebaseInstanceIdTokenRefreshed (token);
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
package com.rmsl.juce;
|
||||
|
||||
import com.google.firebase.messaging.*;
|
||||
|
||||
public final class JuceFirebaseMessagingService extends FirebaseMessagingService
|
||||
{
|
||||
private native void firebaseRemoteMessageReceived (RemoteMessage message);
|
||||
private native void firebaseRemoteMessagesDeleted();
|
||||
private native void firebaseRemoteMessageSent (String messageId);
|
||||
private native void firebaseRemoteMessageSendError (String messageId, String error);
|
||||
|
||||
@Override
|
||||
public void onMessageReceived (RemoteMessage message)
|
||||
{
|
||||
firebaseRemoteMessageReceived (message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeletedMessages()
|
||||
{
|
||||
firebaseRemoteMessagesDeleted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageSent (String messageId)
|
||||
{
|
||||
firebaseRemoteMessageSent (messageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendError (String messageId, Exception e)
|
||||
{
|
||||
firebaseRemoteMessageSendError (messageId, e.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
if (
|
||||
typeof window.__JUCE__ !== "undefined" &&
|
||||
typeof window.__JUCE__.getAndroidUserScripts !== "undefined" &&
|
||||
typeof window.inAndroidUserScriptEval === "undefined"
|
||||
) {
|
||||
window.inAndroidUserScriptEval = true;
|
||||
eval(window.__JUCE__.getAndroidUserScripts());
|
||||
delete window.inAndroidUserScriptEval;
|
||||
}
|
||||
|
||||
{
|
||||
if (typeof window.__JUCE__ === "undefined") {
|
||||
console.warn(
|
||||
"The 'window.__JUCE__' object is undefined." +
|
||||
" Native integration features will not work." +
|
||||
" Defining a placeholder 'window.__JUCE__' object."
|
||||
);
|
||||
|
||||
window.__JUCE__ = {
|
||||
postMessage: function () {},
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof window.__JUCE__.initialisationData === "undefined") {
|
||||
window.__JUCE__.initialisationData = {
|
||||
__juce__platform: [],
|
||||
__juce__functions: [],
|
||||
__juce__registeredGlobalEventIds: [],
|
||||
__juce__sliders: [],
|
||||
__juce__toggles: [],
|
||||
__juce__comboBoxes: [],
|
||||
};
|
||||
}
|
||||
|
||||
class ListenerList {
|
||||
constructor() {
|
||||
this.listeners = new Map();
|
||||
this.listenerId = 0;
|
||||
}
|
||||
|
||||
addListener(fn) {
|
||||
const newListenerId = this.listenerId++;
|
||||
this.listeners.set(newListenerId, fn);
|
||||
return newListenerId;
|
||||
}
|
||||
|
||||
removeListener(id) {
|
||||
if (this.listeners.has(id)) {
|
||||
this.listeners.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
callListeners(payload) {
|
||||
for (const [, value] of this.listeners) {
|
||||
value(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class EventListenerList {
|
||||
constructor() {
|
||||
this.eventListeners = new Map();
|
||||
}
|
||||
|
||||
addEventListener(eventId, fn) {
|
||||
if (!this.eventListeners.has(eventId))
|
||||
this.eventListeners.set(eventId, new ListenerList());
|
||||
|
||||
const id = this.eventListeners.get(eventId).addListener(fn);
|
||||
|
||||
return [eventId, id];
|
||||
}
|
||||
|
||||
removeEventListener([eventId, id]) {
|
||||
if (this.eventListeners.has(eventId)) {
|
||||
this.eventListeners.get(eventId).removeListener(id);
|
||||
}
|
||||
}
|
||||
|
||||
emitEvent(eventId, object) {
|
||||
if (this.eventListeners.has(eventId))
|
||||
this.eventListeners.get(eventId).callListeners(object);
|
||||
}
|
||||
}
|
||||
|
||||
class Backend {
|
||||
constructor() {
|
||||
this.listeners = new EventListenerList();
|
||||
}
|
||||
|
||||
addEventListener(eventId, fn) {
|
||||
return this.listeners.addEventListener(eventId, fn);
|
||||
}
|
||||
|
||||
removeEventListener([eventId, id]) {
|
||||
this.listeners.removeEventListener([eventId, id]);
|
||||
}
|
||||
|
||||
emitEvent(eventId, object) {
|
||||
window.__JUCE__.postMessage(
|
||||
JSON.stringify({ eventId: eventId, payload: object })
|
||||
);
|
||||
}
|
||||
|
||||
emitByBackend(eventId, object) {
|
||||
this.listeners.emitEvent(eventId, JSON.parse(object));
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window.__JUCE__.backend === "undefined")
|
||||
window.__JUCE__.backend = new Backend();
|
||||
}
|
||||
@@ -0,0 +1,577 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
import "./check_native_interop.js";
|
||||
|
||||
class PromiseHandler {
|
||||
constructor() {
|
||||
this.lastPromiseId = 0;
|
||||
this.promises = new Map();
|
||||
|
||||
window.__JUCE__.backend.addEventListener(
|
||||
"__juce__complete",
|
||||
({ promiseId, result }) => {
|
||||
if (this.promises.has(promiseId)) {
|
||||
this.promises.get(promiseId).resolve(result);
|
||||
this.promises.delete(promiseId);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
createPromise() {
|
||||
const promiseId = this.lastPromiseId++;
|
||||
const result = new Promise((resolve, reject) => {
|
||||
this.promises.set(promiseId, { resolve: resolve, reject: reject });
|
||||
});
|
||||
return [promiseId, result];
|
||||
}
|
||||
}
|
||||
|
||||
const promiseHandler = new PromiseHandler();
|
||||
|
||||
/**
|
||||
* Returns a function object that calls a function registered on the JUCE backend and forwards all
|
||||
* parameters to it.
|
||||
*
|
||||
* The provided name should be the same as the name argument passed to
|
||||
* WebBrowserComponent::Options.withNativeFunction() on the backend.
|
||||
*
|
||||
* @param {String} name
|
||||
*/
|
||||
function getNativeFunction(name) {
|
||||
if (!window.__JUCE__.initialisationData.__juce__functions.includes(name))
|
||||
console.warn(
|
||||
`Creating native function binding for '${name}', which is unknown to the backend`
|
||||
);
|
||||
|
||||
const f = function () {
|
||||
const [promiseId, result] = promiseHandler.createPromise();
|
||||
|
||||
window.__JUCE__.backend.emitEvent("__juce__invoke", {
|
||||
name: name,
|
||||
params: Array.prototype.slice.call(arguments),
|
||||
resultId: promiseId,
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
||||
class ListenerList {
|
||||
constructor() {
|
||||
this.listeners = new Map();
|
||||
this.listenerId = 0;
|
||||
}
|
||||
|
||||
addListener(fn) {
|
||||
const newListenerId = this.listenerId++;
|
||||
this.listeners.set(newListenerId, fn);
|
||||
return newListenerId;
|
||||
}
|
||||
|
||||
removeListener(id) {
|
||||
if (this.listeners.has(id)) {
|
||||
this.listeners.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
callListeners(payload) {
|
||||
for (const [, value] of this.listeners) {
|
||||
value(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const BasicControl_valueChangedEventId = "valueChanged";
|
||||
const BasicControl_propertiesChangedId = "propertiesChanged";
|
||||
const SliderControl_sliderDragStartedEventId = "sliderDragStarted";
|
||||
const SliderControl_sliderDragEndedEventId = "sliderDragEnded";
|
||||
|
||||
/**
|
||||
* SliderState encapsulates data and callbacks that are synchronised with a WebSliderRelay object
|
||||
* on the backend.
|
||||
*
|
||||
* Use getSliderState() to create a SliderState object. This object will be synchronised with the
|
||||
* WebSliderRelay backend object that was created using the same unique name.
|
||||
*
|
||||
* @param {String} name
|
||||
*/
|
||||
class SliderState {
|
||||
constructor(name) {
|
||||
if (!window.__JUCE__.initialisationData.__juce__sliders.includes(name))
|
||||
console.warn(
|
||||
"Creating SliderState for '" +
|
||||
name +
|
||||
"', which is unknown to the backend"
|
||||
);
|
||||
|
||||
this.name = name;
|
||||
this.identifier = "__juce__slider" + this.name;
|
||||
this.scaledValue = 0;
|
||||
this.properties = {
|
||||
start: 0,
|
||||
end: 1,
|
||||
skew: 1,
|
||||
name: "",
|
||||
label: "",
|
||||
numSteps: 100,
|
||||
interval: 0,
|
||||
parameterIndex: -1,
|
||||
};
|
||||
this.valueChangedEvent = new ListenerList();
|
||||
this.propertiesChangedEvent = new ListenerList();
|
||||
|
||||
window.__JUCE__.backend.addEventListener(this.identifier, (event) =>
|
||||
this.handleEvent(event)
|
||||
);
|
||||
|
||||
window.__JUCE__.backend.emitEvent(this.identifier, {
|
||||
eventType: "requestInitialUpdate",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the normalised value of the corresponding backend parameter. This value is always in the
|
||||
* [0, 1] range (inclusive).
|
||||
*
|
||||
* The meaning of this range is the same as in the case of
|
||||
* AudioProcessorParameter::getValue() (C++).
|
||||
*
|
||||
* @param {String} name
|
||||
*/
|
||||
setNormalisedValue(newValue) {
|
||||
this.scaledValue = this.snapToLegalValue(
|
||||
this.normalisedToScaledValue(newValue)
|
||||
);
|
||||
|
||||
window.__JUCE__.backend.emitEvent(this.identifier, {
|
||||
eventType: BasicControl_valueChangedEventId,
|
||||
value: this.scaledValue,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This function should be called first thing when the user starts interacting with the slider.
|
||||
*/
|
||||
sliderDragStarted() {
|
||||
window.__JUCE__.backend.emitEvent(this.identifier, {
|
||||
eventType: SliderControl_sliderDragStartedEventId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This function should be called when the user finished the interaction with the slider.
|
||||
*/
|
||||
sliderDragEnded() {
|
||||
window.__JUCE__.backend.emitEvent(this.identifier, {
|
||||
eventType: SliderControl_sliderDragEndedEventId,
|
||||
});
|
||||
}
|
||||
|
||||
/** Internal. */
|
||||
handleEvent(event) {
|
||||
if (event.eventType == BasicControl_valueChangedEventId) {
|
||||
this.scaledValue = event.value;
|
||||
this.valueChangedEvent.callListeners();
|
||||
}
|
||||
if (event.eventType == BasicControl_propertiesChangedId) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let { eventType: _, ...rest } = event;
|
||||
this.properties = rest;
|
||||
this.propertiesChangedEvent.callListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the scaled value of the parameter. This corresponds to the return value of
|
||||
* NormalisableRange::convertFrom0to1() (C++). This value will differ from a linear
|
||||
* [0, 1] range if a non-default NormalisableRange was set for the parameter.
|
||||
*/
|
||||
getScaledValue() {
|
||||
return this.scaledValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the normalised value of the corresponding backend parameter. This value is always in the
|
||||
* [0, 1] range (inclusive).
|
||||
*
|
||||
* The meaning of this range is the same as in the case of
|
||||
* AudioProcessorParameter::getValue() (C++).
|
||||
*
|
||||
* @param {String} name
|
||||
*/
|
||||
getNormalisedValue() {
|
||||
return Math.pow(
|
||||
(this.scaledValue - this.properties.start) /
|
||||
(this.properties.end - this.properties.start),
|
||||
this.properties.skew
|
||||
);
|
||||
}
|
||||
|
||||
/** Internal. */
|
||||
normalisedToScaledValue(normalisedValue) {
|
||||
return (
|
||||
Math.pow(normalisedValue, 1 / this.properties.skew) *
|
||||
(this.properties.end - this.properties.start) +
|
||||
this.properties.start
|
||||
);
|
||||
}
|
||||
|
||||
/** Internal. */
|
||||
snapToLegalValue(value) {
|
||||
const interval = this.properties.interval;
|
||||
|
||||
if (interval == 0) return value;
|
||||
|
||||
const start = this.properties.start;
|
||||
const clamp = (val, min = 0, max = 1) => Math.max(min, Math.min(max, val));
|
||||
|
||||
return clamp(
|
||||
start + interval * Math.floor((value - start) / interval + 0.5),
|
||||
this.properties.start,
|
||||
this.properties.end
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const sliderStates = new Map();
|
||||
|
||||
for (const sliderName of window.__JUCE__.initialisationData.__juce__sliders)
|
||||
sliderStates.set(sliderName, new SliderState(sliderName));
|
||||
|
||||
/**
|
||||
* Returns a SliderState object that is connected to the backend WebSliderRelay object that was
|
||||
* created with the same name argument.
|
||||
*
|
||||
* To register a WebSliderRelay object create one with the right name and add it to the
|
||||
* WebBrowserComponent::Options struct using withOptionsFrom.
|
||||
*
|
||||
* @param {String} name
|
||||
*/
|
||||
function getSliderState(name) {
|
||||
if (!sliderStates.has(name)) sliderStates.set(name, new SliderState(name));
|
||||
|
||||
return sliderStates.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* ToggleState encapsulates data and callbacks that are synchronised with a WebToggleRelay object
|
||||
* on the backend.
|
||||
*
|
||||
* Use getToggleState() to create a ToggleState object. This object will be synchronised with the
|
||||
* WebToggleRelay backend object that was created using the same unique name.
|
||||
*
|
||||
* @param {String} name
|
||||
*/
|
||||
class ToggleState {
|
||||
constructor(name) {
|
||||
if (!window.__JUCE__.initialisationData.__juce__toggles.includes(name))
|
||||
console.warn(
|
||||
"Creating ToggleState for '" +
|
||||
name +
|
||||
"', which is unknown to the backend"
|
||||
);
|
||||
|
||||
this.name = name;
|
||||
this.identifier = "__juce__toggle" + this.name;
|
||||
this.value = false;
|
||||
this.properties = {
|
||||
name: "",
|
||||
parameterIndex: -1,
|
||||
};
|
||||
this.valueChangedEvent = new ListenerList();
|
||||
this.propertiesChangedEvent = new ListenerList();
|
||||
|
||||
window.__JUCE__.backend.addEventListener(this.identifier, (event) =>
|
||||
this.handleEvent(event)
|
||||
);
|
||||
|
||||
window.__JUCE__.backend.emitEvent(this.identifier, {
|
||||
eventType: "requestInitialUpdate",
|
||||
});
|
||||
}
|
||||
|
||||
/** Returns the value corresponding to the associated WebToggleRelay's (C++) state. */
|
||||
getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
/** Informs the backend to change the associated WebToggleRelay's (C++) state. */
|
||||
setValue(newValue) {
|
||||
this.value = newValue;
|
||||
|
||||
window.__JUCE__.backend.emitEvent(this.identifier, {
|
||||
eventType: BasicControl_valueChangedEventId,
|
||||
value: this.value,
|
||||
});
|
||||
}
|
||||
|
||||
/** Internal. */
|
||||
handleEvent(event) {
|
||||
if (event.eventType == BasicControl_valueChangedEventId) {
|
||||
this.value = event.value;
|
||||
this.valueChangedEvent.callListeners();
|
||||
}
|
||||
if (event.eventType == BasicControl_propertiesChangedId) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let { eventType: _, ...rest } = event;
|
||||
this.properties = rest;
|
||||
this.propertiesChangedEvent.callListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toggleStates = new Map();
|
||||
|
||||
for (const name of window.__JUCE__.initialisationData.__juce__toggles)
|
||||
toggleStates.set(name, new ToggleState(name));
|
||||
|
||||
/**
|
||||
* Returns a ToggleState object that is connected to the backend WebToggleButtonRelay object that was
|
||||
* created with the same name argument.
|
||||
*
|
||||
* To register a WebToggleButtonRelay object create one with the right name and add it to the
|
||||
* WebBrowserComponent::Options struct using withOptionsFrom.
|
||||
*
|
||||
* @param {String} name
|
||||
*/
|
||||
function getToggleState(name) {
|
||||
if (!toggleStates.has(name)) toggleStates.set(name, new ToggleState(name));
|
||||
|
||||
return toggleStates.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* ComboBoxState encapsulates data and callbacks that are synchronised with a WebComboBoxRelay object
|
||||
* on the backend.
|
||||
*
|
||||
* Use getComboBoxState() to create a ComboBoxState object. This object will be synchronised with the
|
||||
* WebComboBoxRelay backend object that was created using the same unique name.
|
||||
*
|
||||
* @param {String} name
|
||||
*/
|
||||
class ComboBoxState {
|
||||
constructor(name) {
|
||||
if (!window.__JUCE__.initialisationData.__juce__comboBoxes.includes(name))
|
||||
console.warn(
|
||||
"Creating ComboBoxState for '" +
|
||||
name +
|
||||
"', which is unknown to the backend"
|
||||
);
|
||||
|
||||
this.name = name;
|
||||
this.identifier = "__juce__comboBox" + this.name;
|
||||
this.value = 0.0;
|
||||
this.properties = {
|
||||
name: "",
|
||||
parameterIndex: -1,
|
||||
choices: [],
|
||||
};
|
||||
this.valueChangedEvent = new ListenerList();
|
||||
this.propertiesChangedEvent = new ListenerList();
|
||||
|
||||
window.__JUCE__.backend.addEventListener(this.identifier, (event) =>
|
||||
this.handleEvent(event)
|
||||
);
|
||||
|
||||
window.__JUCE__.backend.emitEvent(this.identifier, {
|
||||
eventType: "requestInitialUpdate",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value corresponding to the associated WebComboBoxRelay's (C++) state.
|
||||
*
|
||||
* This is an index identifying which element of the properties.choices array is currently
|
||||
* selected.
|
||||
*/
|
||||
getChoiceIndex() {
|
||||
return Math.round(this.value * (this.properties.choices.length - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Informs the backend to change the associated WebComboBoxRelay's (C++) state.
|
||||
*
|
||||
* This should be called with the index identifying the selected element from the
|
||||
* properties.choices array.
|
||||
*/
|
||||
setChoiceIndex(index) {
|
||||
const numItems = this.properties.choices.length;
|
||||
this.value = numItems > 1 ? index / (numItems - 1) : 0.0;
|
||||
|
||||
window.__JUCE__.backend.emitEvent(this.identifier, {
|
||||
eventType: BasicControl_valueChangedEventId,
|
||||
value: this.value,
|
||||
});
|
||||
}
|
||||
|
||||
/** Internal. */
|
||||
handleEvent(event) {
|
||||
if (event.eventType == BasicControl_valueChangedEventId) {
|
||||
this.value = event.value;
|
||||
this.valueChangedEvent.callListeners();
|
||||
}
|
||||
if (event.eventType == BasicControl_propertiesChangedId) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let { eventType: _, ...rest } = event;
|
||||
this.properties = rest;
|
||||
this.propertiesChangedEvent.callListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const comboBoxStates = new Map();
|
||||
|
||||
for (const name of window.__JUCE__.initialisationData.__juce__comboBoxes)
|
||||
comboBoxStates.set(name, new ComboBoxState(name));
|
||||
|
||||
/**
|
||||
* Returns a ComboBoxState object that is connected to the backend WebComboBoxRelay object that was
|
||||
* created with the same name argument.
|
||||
*
|
||||
* To register a WebComboBoxRelay object create one with the right name and add it to the
|
||||
* WebBrowserComponent::Options struct using withOptionsFrom.
|
||||
*
|
||||
* @param {String} name
|
||||
*/
|
||||
function getComboBoxState(name) {
|
||||
if (!comboBoxStates.has(name))
|
||||
comboBoxStates.set(name, new ComboBoxState(name));
|
||||
|
||||
return comboBoxStates.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a platform-specific prefix to the path to ensure that a request sent to this address will
|
||||
* be received by the backend's ResourceProvider.
|
||||
* @param {String} path
|
||||
*/
|
||||
function getBackendResourceAddress(path) {
|
||||
const platform =
|
||||
window.__JUCE__.initialisationData.__juce__platform.length > 0
|
||||
? window.__JUCE__.initialisationData.__juce__platform[0]
|
||||
: "";
|
||||
|
||||
if (platform == "windows" || platform == "android")
|
||||
return "https://juce.backend/" + path;
|
||||
|
||||
if (platform == "macos" || platform == "ios" || platform == "linux")
|
||||
return "juce://juce.backend/" + path;
|
||||
|
||||
console.warn(
|
||||
"getBackendResourceAddress() called, but no JUCE native backend is detected."
|
||||
);
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* This helper class is intended to aid the implementation of
|
||||
* AudioProcessorEditor::getControlParameterIndex() for editors using a WebView interface.
|
||||
*
|
||||
* Create an instance of this class and call its handleMouseMove() method in each mousemove event.
|
||||
*
|
||||
* This class can be used to continuously report the controlParameterIndexAnnotation attribute's
|
||||
* value related to the DOM element that is currently under the mouse pointer.
|
||||
*
|
||||
* This value is defined at all times as follows
|
||||
* * the annotation attribute's value for the DOM element directly under the mouse, if it has it,
|
||||
* * the annotation attribute's value for the first parent element, that has it,
|
||||
* * -1 otherwise.
|
||||
*
|
||||
* Whenever there is a change in this value, an event is emitted to the frontend with the new value.
|
||||
* You can use a ControlParameterIndexReceiver object on the backend to listen to these events.
|
||||
*
|
||||
* @param {String} controlParameterIndexAnnotation
|
||||
*/
|
||||
class ControlParameterIndexUpdater {
|
||||
constructor(controlParameterIndexAnnotation) {
|
||||
this.controlParameterIndexAnnotation = controlParameterIndexAnnotation;
|
||||
this.lastElement = null;
|
||||
this.lastControlParameterIndex = null;
|
||||
}
|
||||
|
||||
handleMouseMove(event) {
|
||||
const currentElement = document.elementFromPoint(
|
||||
event.clientX,
|
||||
event.clientY
|
||||
);
|
||||
|
||||
if (currentElement === this.lastElement) return;
|
||||
this.lastElement = currentElement;
|
||||
|
||||
let controlParameterIndex = -1;
|
||||
|
||||
if (currentElement !== null)
|
||||
controlParameterIndex = this.#getControlParameterIndex(currentElement);
|
||||
|
||||
if (controlParameterIndex === this.lastControlParameterIndex) return;
|
||||
this.lastControlParameterIndex = controlParameterIndex;
|
||||
|
||||
window.__JUCE__.backend.emitEvent(
|
||||
"__juce__controlParameterIndexChanged",
|
||||
controlParameterIndex
|
||||
);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
#getControlParameterIndex(element) {
|
||||
const isValidNonRootElement = (e) => {
|
||||
return e !== null && e !== document.documentElement;
|
||||
};
|
||||
|
||||
while (isValidNonRootElement(element)) {
|
||||
if (element.hasAttribute(this.controlParameterIndexAnnotation)) {
|
||||
return element.getAttribute(this.controlParameterIndexAnnotation);
|
||||
}
|
||||
|
||||
element = element.parentElement;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
getNativeFunction,
|
||||
getSliderState,
|
||||
getToggleState,
|
||||
getComboBoxState,
|
||||
getBackendResourceAddress,
|
||||
ControlParameterIndexUpdater,
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "juce-framework-frontend",
|
||||
"version": "7.0.7"
|
||||
}
|
||||
@@ -0,0 +1,553 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
extern int64 getMouseEventTime();
|
||||
|
||||
JUCE_DECLARE_UUID_GETTER (IOleObject, "00000112-0000-0000-C000-000000000046")
|
||||
JUCE_DECLARE_UUID_GETTER (IOleWindow, "00000114-0000-0000-C000-000000000046")
|
||||
JUCE_DECLARE_UUID_GETTER (IOleInPlaceSite, "00000119-0000-0000-C000-000000000046")
|
||||
|
||||
namespace ActiveXHelpers
|
||||
{
|
||||
//==============================================================================
|
||||
struct JuceIStorage final : public ComBaseClassHelper<IStorage>
|
||||
{
|
||||
JuceIStorage() = default;
|
||||
|
||||
JUCE_COMRESULT CreateStream (const WCHAR*, DWORD, DWORD, DWORD, IStream**) override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT OpenStream (const WCHAR*, void*, DWORD, DWORD, IStream**) override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT CreateStorage (const WCHAR*, DWORD, DWORD, DWORD, IStorage**) override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT OpenStorage (const WCHAR*, IStorage*, DWORD, SNB, DWORD, IStorage**) override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT CopyTo (DWORD, IID const*, SNB, IStorage*) override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT MoveElementTo (const OLECHAR*,IStorage*, const OLECHAR*, DWORD) override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT Commit (DWORD) override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT Revert() override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT EnumElements (DWORD, void*, DWORD, IEnumSTATSTG**) override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT DestroyElement (const OLECHAR*) override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT RenameElement (const WCHAR*, const WCHAR*) override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT SetElementTimes (const WCHAR*, FILETIME const*, FILETIME const*, FILETIME const*) override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT SetClass (REFCLSID) override { return S_OK; }
|
||||
JUCE_COMRESULT SetStateBits (DWORD, DWORD) override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT Stat (STATSTG*, DWORD) override { return E_NOTIMPL; }
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
struct JuceOleInPlaceFrame final : public ComBaseClassHelper<IOleInPlaceFrame>
|
||||
{
|
||||
JuceOleInPlaceFrame (HWND hwnd) : window (hwnd) {}
|
||||
|
||||
JUCE_COMRESULT GetWindow (HWND* lphwnd) override { *lphwnd = window; return S_OK; }
|
||||
JUCE_COMRESULT ContextSensitiveHelp (BOOL) override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT GetBorder (LPRECT) override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT RequestBorderSpace (LPCBORDERWIDTHS) override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT SetBorderSpace (LPCBORDERWIDTHS) override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT SetActiveObject (IOleInPlaceActiveObject* a, LPCOLESTR) override { activeObject = addComSmartPtrOwner (a); return S_OK; }
|
||||
JUCE_COMRESULT InsertMenus (HMENU, LPOLEMENUGROUPWIDTHS) override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT SetMenu (HMENU, HOLEMENU, HWND) override { return S_OK; }
|
||||
JUCE_COMRESULT RemoveMenus (HMENU) override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT SetStatusText (LPCOLESTR) override { return S_OK; }
|
||||
JUCE_COMRESULT EnableModeless (BOOL) override { return S_OK; }
|
||||
JUCE_COMRESULT TranslateAccelerator (LPMSG, WORD) override { return E_NOTIMPL; }
|
||||
|
||||
HRESULT OfferKeyTranslation (LPMSG lpmsg)
|
||||
{
|
||||
if (activeObject != nullptr)
|
||||
return activeObject->TranslateAcceleratorW (lpmsg);
|
||||
|
||||
return S_FALSE;
|
||||
}
|
||||
|
||||
HWND window;
|
||||
ComSmartPtr<IOleInPlaceActiveObject> activeObject;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
struct JuceIOleInPlaceSite final : public ComBaseClassHelper<IOleInPlaceSite>
|
||||
{
|
||||
JuceIOleInPlaceSite (HWND hwnd)
|
||||
: window (hwnd),
|
||||
frame (new JuceOleInPlaceFrame (window))
|
||||
{}
|
||||
|
||||
~JuceIOleInPlaceSite() override
|
||||
{
|
||||
frame->Release();
|
||||
}
|
||||
|
||||
JUCE_COMRESULT GetWindow (HWND* lphwnd) override { *lphwnd = window; return S_OK; }
|
||||
JUCE_COMRESULT ContextSensitiveHelp (BOOL) override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT CanInPlaceActivate() override { return S_OK; }
|
||||
JUCE_COMRESULT OnInPlaceActivate() override { return S_OK; }
|
||||
JUCE_COMRESULT OnUIActivate() override { return S_OK; }
|
||||
|
||||
JUCE_COMRESULT GetWindowContext (LPOLEINPLACEFRAME* lplpFrame, LPOLEINPLACEUIWINDOW* lplpDoc, LPRECT, LPRECT, LPOLEINPLACEFRAMEINFO lpFrameInfo) override
|
||||
{
|
||||
/* Note: If you call AddRef on the frame here, then some types of object (e.g. web browser control) cause leaks..
|
||||
If you don't call AddRef then others crash (e.g. QuickTime).. Bit of a catch-22, so letting it leak is probably preferable.
|
||||
*/
|
||||
if (lplpFrame != nullptr) { frame->AddRef(); *lplpFrame = frame; }
|
||||
if (lplpDoc != nullptr) *lplpDoc = nullptr;
|
||||
lpFrameInfo->fMDIApp = FALSE;
|
||||
lpFrameInfo->hwndFrame = window;
|
||||
lpFrameInfo->haccel = nullptr;
|
||||
lpFrameInfo->cAccelEntries = 0;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
JUCE_COMRESULT Scroll (SIZE) override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT OnUIDeactivate (BOOL) override { return S_OK; }
|
||||
JUCE_COMRESULT OnInPlaceDeactivate() override { return S_OK; }
|
||||
JUCE_COMRESULT DiscardUndoState() override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT DeactivateAndUndo() override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT OnPosRectChange (LPCRECT) override { return S_OK; }
|
||||
|
||||
LRESULT offerEventToActiveXControl (::MSG& msg)
|
||||
{
|
||||
if (frame != nullptr)
|
||||
return frame->OfferKeyTranslation (&msg);
|
||||
|
||||
return S_FALSE;
|
||||
}
|
||||
|
||||
HWND window;
|
||||
JuceOleInPlaceFrame* frame;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
struct JuceIOleClientSite final : public ComBaseClassHelper<IOleClientSite>
|
||||
{
|
||||
JuceIOleClientSite (HWND window) : inplaceSite (new JuceIOleInPlaceSite (window))
|
||||
{}
|
||||
|
||||
~JuceIOleClientSite() override
|
||||
{
|
||||
inplaceSite->Release();
|
||||
|
||||
if (dispatchEventHandler != nullptr)
|
||||
{
|
||||
dispatchEventHandler->Release();
|
||||
dispatchEventHandler = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
JUCE_COMRESULT QueryInterface (REFIID type, void** result) override
|
||||
{
|
||||
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token")
|
||||
|
||||
if (type == __uuidof (IOleInPlaceSite))
|
||||
{
|
||||
inplaceSite->AddRef();
|
||||
*result = static_cast<IOleInPlaceSite*> (inplaceSite);
|
||||
return S_OK;
|
||||
}
|
||||
else if (type == __uuidof (IDispatch) && dispatchEventHandler != nullptr)
|
||||
{
|
||||
dispatchEventHandler->AddRef();
|
||||
*result = dispatchEventHandler;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
return ComBaseClassHelper <IOleClientSite>::QueryInterface (type, result);
|
||||
|
||||
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||||
}
|
||||
|
||||
JUCE_COMRESULT SaveObject() override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT GetMoniker (DWORD, DWORD, IMoniker**) override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT GetContainer (LPOLECONTAINER* ppContainer) override { *ppContainer = nullptr; return E_NOINTERFACE; }
|
||||
JUCE_COMRESULT ShowObject() override { return S_OK; }
|
||||
JUCE_COMRESULT OnShowWindow (BOOL) override { return E_NOTIMPL; }
|
||||
JUCE_COMRESULT RequestNewObjectLayout() override { return E_NOTIMPL; }
|
||||
|
||||
LRESULT offerEventToActiveXControl (::MSG& msg)
|
||||
{
|
||||
if (inplaceSite != nullptr)
|
||||
return inplaceSite->offerEventToActiveXControl (msg);
|
||||
|
||||
return S_FALSE;
|
||||
}
|
||||
|
||||
void setEventHandler (void* eventHandler)
|
||||
{
|
||||
IDispatch* newEventHandler = nullptr;
|
||||
|
||||
if (eventHandler != nullptr)
|
||||
{
|
||||
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token")
|
||||
|
||||
auto iidIDispatch = __uuidof (IDispatch);
|
||||
|
||||
if (static_cast<IUnknown*> (eventHandler)->QueryInterface (iidIDispatch, (void**) &newEventHandler) != S_OK
|
||||
|| newEventHandler == nullptr)
|
||||
return;
|
||||
|
||||
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||||
}
|
||||
|
||||
if (dispatchEventHandler != nullptr)
|
||||
dispatchEventHandler->Release();
|
||||
|
||||
dispatchEventHandler = newEventHandler;
|
||||
}
|
||||
|
||||
JuceIOleInPlaceSite* inplaceSite;
|
||||
IDispatch* dispatchEventHandler = nullptr;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
static Array<ActiveXControlComponent*> activeXComps;
|
||||
|
||||
static HWND getHWND (const ActiveXControlComponent* const component)
|
||||
{
|
||||
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token")
|
||||
|
||||
HWND hwnd = {};
|
||||
const IID iid = __uuidof (IOleWindow);
|
||||
|
||||
if (auto* window = (IOleWindow*) component->queryInterface (&iid))
|
||||
{
|
||||
window->GetWindow (&hwnd);
|
||||
window->Release();
|
||||
}
|
||||
|
||||
return hwnd;
|
||||
|
||||
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||||
}
|
||||
|
||||
static void offerActiveXMouseEventToPeer (ComponentPeer* peer, HWND hwnd, UINT message, LPARAM lParam)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case WM_MOUSEMOVE:
|
||||
case WM_LBUTTONDOWN:
|
||||
case WM_MBUTTONDOWN:
|
||||
case WM_RBUTTONDOWN:
|
||||
case WM_LBUTTONUP:
|
||||
case WM_MBUTTONUP:
|
||||
case WM_RBUTTONUP:
|
||||
{
|
||||
RECT activeXRect, peerRect;
|
||||
GetWindowRect (hwnd, &activeXRect);
|
||||
GetWindowRect ((HWND) peer->getNativeHandle(), &peerRect);
|
||||
|
||||
peer->handleMouseEvent (MouseInputSource::InputSourceType::mouse,
|
||||
{ (float) (GET_X_LPARAM (lParam) + activeXRect.left - peerRect.left),
|
||||
(float) (GET_Y_LPARAM (lParam) + activeXRect.top - peerRect.top) },
|
||||
ComponentPeer::getCurrentModifiersRealtime(),
|
||||
MouseInputSource::defaultPressure,
|
||||
MouseInputSource::defaultOrientation,
|
||||
getMouseEventTime());
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
class ActiveXControlComponent::Pimpl : public ComponentMovementWatcher,
|
||||
public ComponentPeer::ScaleFactorListener
|
||||
{
|
||||
public:
|
||||
Pimpl (HWND hwnd, ActiveXControlComponent& activeXComp)
|
||||
: ComponentMovementWatcher (&activeXComp),
|
||||
owner (activeXComp),
|
||||
storage (new ActiveXHelpers::JuceIStorage()),
|
||||
clientSite (new ActiveXHelpers::JuceIOleClientSite (hwnd))
|
||||
{
|
||||
}
|
||||
|
||||
~Pimpl() override
|
||||
{
|
||||
// If the wndproc of the ActiveX HWND isn't set back to it's original
|
||||
// wndproc, then clientSite will leak when control is released
|
||||
if (controlHWND != nullptr)
|
||||
SetWindowLongPtr ((HWND) controlHWND, GWLP_WNDPROC, (LONG_PTR) originalWndProc);
|
||||
|
||||
if (control != nullptr)
|
||||
{
|
||||
control->Close (OLECLOSE_NOSAVE);
|
||||
control->Release();
|
||||
}
|
||||
|
||||
clientSite->Release();
|
||||
storage->Release();
|
||||
|
||||
if (currentPeer != nullptr)
|
||||
currentPeer->removeScaleFactorListener (this);
|
||||
}
|
||||
|
||||
void setControlBounds (Rectangle<int> newBounds) const
|
||||
{
|
||||
if (controlHWND != nullptr)
|
||||
{
|
||||
if (auto* peer = owner.getTopLevelComponent()->getPeer())
|
||||
newBounds = (newBounds.toDouble() * peer->getPlatformScaleFactor()).toNearestInt();
|
||||
|
||||
MoveWindow (controlHWND, newBounds.getX(), newBounds.getY(), newBounds.getWidth(), newBounds.getHeight(), TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
void setControlVisible (bool shouldBeVisible) const
|
||||
{
|
||||
if (controlHWND != nullptr)
|
||||
ShowWindow (controlHWND, shouldBeVisible ? SW_SHOWNA : SW_HIDE);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
using ComponentMovementWatcher::componentMovedOrResized;
|
||||
|
||||
void componentMovedOrResized (bool /*wasMoved*/, bool /*wasResized*/) override
|
||||
{
|
||||
if (auto* peer = owner.getTopLevelComponent()->getPeer())
|
||||
setControlBounds (peer->getAreaCoveredBy (owner));
|
||||
}
|
||||
|
||||
void componentPeerChanged() override
|
||||
{
|
||||
if (currentPeer != nullptr)
|
||||
currentPeer->removeScaleFactorListener (this);
|
||||
|
||||
componentMovedOrResized (true, true);
|
||||
|
||||
currentPeer = owner.getTopLevelComponent()->getPeer();
|
||||
|
||||
if (currentPeer != nullptr)
|
||||
currentPeer->addScaleFactorListener (this);
|
||||
}
|
||||
|
||||
using ComponentMovementWatcher::componentVisibilityChanged;
|
||||
|
||||
void componentVisibilityChanged() override
|
||||
{
|
||||
setControlVisible (owner.isShowing());
|
||||
componentPeerChanged();
|
||||
}
|
||||
|
||||
void nativeScaleFactorChanged (double /*newScaleFactor*/) override
|
||||
{
|
||||
componentMovedOrResized (true, true);
|
||||
}
|
||||
|
||||
// intercepts events going to an activeX control, so we can sneakily use the mouse events
|
||||
static LRESULT CALLBACK activeXHookWndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
for (auto* ax : ActiveXHelpers::activeXComps)
|
||||
{
|
||||
if (ax->control != nullptr && ax->control->controlHWND == hwnd)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case WM_MOUSEMOVE:
|
||||
case WM_LBUTTONDOWN:
|
||||
case WM_MBUTTONDOWN:
|
||||
case WM_RBUTTONDOWN:
|
||||
case WM_LBUTTONUP:
|
||||
case WM_MBUTTONUP:
|
||||
case WM_RBUTTONUP:
|
||||
case WM_LBUTTONDBLCLK:
|
||||
case WM_MBUTTONDBLCLK:
|
||||
case WM_RBUTTONDBLCLK:
|
||||
if (ax->isShowing())
|
||||
{
|
||||
if (auto* peer = ax->getPeer())
|
||||
{
|
||||
ActiveXHelpers::offerActiveXMouseEventToPeer (peer, hwnd, message, lParam);
|
||||
|
||||
if (! ax->areMouseEventsAllowed())
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return CallWindowProc (ax->control->originalWndProc, hwnd, message, wParam, lParam);
|
||||
}
|
||||
}
|
||||
|
||||
return DefWindowProc (hwnd, message, wParam, lParam);
|
||||
}
|
||||
|
||||
ActiveXControlComponent& owner;
|
||||
ComponentPeer* currentPeer = nullptr;
|
||||
HWND controlHWND = {};
|
||||
IStorage* storage = nullptr;
|
||||
ActiveXHelpers::JuceIOleClientSite* clientSite = nullptr;
|
||||
IOleObject* control = nullptr;
|
||||
WNDPROC originalWndProc = nullptr;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
ActiveXControlComponent::ActiveXControlComponent()
|
||||
{
|
||||
ActiveXHelpers::activeXComps.add (this);
|
||||
}
|
||||
|
||||
ActiveXControlComponent::~ActiveXControlComponent()
|
||||
{
|
||||
deleteControl();
|
||||
ActiveXHelpers::activeXComps.removeFirstMatchingValue (this);
|
||||
}
|
||||
|
||||
void ActiveXControlComponent::paint (Graphics& g)
|
||||
{
|
||||
if (control == nullptr)
|
||||
g.fillAll (Colours::lightgrey);
|
||||
}
|
||||
|
||||
bool ActiveXControlComponent::createControl (const void* controlIID)
|
||||
{
|
||||
deleteControl();
|
||||
|
||||
if (auto* peer = getPeer())
|
||||
{
|
||||
auto controlBounds = peer->getAreaCoveredBy (*this);
|
||||
auto hwnd = (HWND) peer->getNativeHandle();
|
||||
|
||||
std::unique_ptr<Pimpl> newControl (new Pimpl (hwnd, *this));
|
||||
|
||||
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wlanguage-extension-token")
|
||||
|
||||
HRESULT hr = OleCreate (*(const IID*) controlIID, __uuidof (IOleObject), 1 /*OLERENDER_DRAW*/, nullptr,
|
||||
newControl->clientSite, newControl->storage,
|
||||
(void**) &(newControl->control));
|
||||
|
||||
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||||
|
||||
if (hr == S_OK)
|
||||
{
|
||||
newControl->control->SetHostNames (L"JUCE", nullptr);
|
||||
|
||||
if (OleSetContainedObject (newControl->control, TRUE) == S_OK)
|
||||
{
|
||||
RECT rect;
|
||||
rect.left = controlBounds.getX();
|
||||
rect.top = controlBounds.getY();
|
||||
rect.right = controlBounds.getRight();
|
||||
rect.bottom = controlBounds.getBottom();
|
||||
|
||||
if (newControl->control->DoVerb (OLEIVERB_SHOW, nullptr, newControl->clientSite, 0, hwnd, &rect) == S_OK)
|
||||
{
|
||||
control = std::move (newControl);
|
||||
control->controlHWND = ActiveXHelpers::getHWND (this);
|
||||
|
||||
if (control->controlHWND != nullptr)
|
||||
{
|
||||
control->setControlBounds (controlBounds);
|
||||
|
||||
control->originalWndProc = (WNDPROC) GetWindowLongPtr ((HWND) control->controlHWND, GWLP_WNDPROC);
|
||||
SetWindowLongPtr ((HWND) control->controlHWND, GWLP_WNDPROC, (LONG_PTR) Pimpl::activeXHookWndProc);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// the component must have already been added to a real window when you call this!
|
||||
jassertfalse;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ActiveXControlComponent::deleteControl()
|
||||
{
|
||||
control = nullptr;
|
||||
}
|
||||
|
||||
void* ActiveXControlComponent::queryInterface (const void* iid) const
|
||||
{
|
||||
void* result = nullptr;
|
||||
|
||||
if (control != nullptr && control->control != nullptr
|
||||
&& SUCCEEDED (control->control->QueryInterface (*(const IID*) iid, &result)))
|
||||
return result;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ActiveXControlComponent::setMouseEventsAllowed (const bool eventsCanReachControl)
|
||||
{
|
||||
mouseEventsAllowed = eventsCanReachControl;
|
||||
}
|
||||
|
||||
intptr_t ActiveXControlComponent::offerEventToActiveXControl (void* ptr)
|
||||
{
|
||||
if (control != nullptr && control->clientSite != nullptr)
|
||||
return (intptr_t) control->clientSite->offerEventToActiveXControl (*reinterpret_cast<::MSG*> (ptr));
|
||||
|
||||
return S_FALSE;
|
||||
}
|
||||
|
||||
intptr_t ActiveXControlComponent::offerEventToActiveXControlStatic (void* ptr)
|
||||
{
|
||||
for (auto* ax : ActiveXHelpers::activeXComps)
|
||||
{
|
||||
auto result = ax->offerEventToActiveXControl (ptr);
|
||||
|
||||
if (result != S_FALSE)
|
||||
return result;
|
||||
}
|
||||
|
||||
return S_FALSE;
|
||||
}
|
||||
|
||||
void ActiveXControlComponent::setEventHandler (void* eventHandler)
|
||||
{
|
||||
if (control->clientSite != nullptr)
|
||||
control->clientSite->setEventHandler (eventHandler);
|
||||
}
|
||||
|
||||
LRESULT juce_offerEventToActiveXControl (::MSG& msg);
|
||||
LRESULT juce_offerEventToActiveXControl (::MSG& msg)
|
||||
{
|
||||
if (msg.message >= WM_KEYFIRST && msg.message <= WM_KEYLAST)
|
||||
return ActiveXControlComponent::offerEventToActiveXControlStatic (&msg);
|
||||
|
||||
return S_FALSE;
|
||||
}
|
||||
|
||||
} // namespace juce
|
||||
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
class AndroidViewComponent::Pimpl : public ComponentMovementWatcher
|
||||
{
|
||||
public:
|
||||
Pimpl (const LocalRef<jobject>& v, Component& comp)
|
||||
: ComponentMovementWatcher (&comp),
|
||||
view (v),
|
||||
owner (comp)
|
||||
{
|
||||
if (owner.isShowing())
|
||||
componentPeerChanged();
|
||||
}
|
||||
|
||||
~Pimpl() override
|
||||
{
|
||||
removeFromParent();
|
||||
}
|
||||
|
||||
void componentMovedOrResized (bool /*wasMoved*/, bool /*wasResized*/) override
|
||||
{
|
||||
auto* topComp = owner.getTopLevelComponent();
|
||||
|
||||
if (topComp->getPeer() != nullptr)
|
||||
{
|
||||
auto pos = topComp->getLocalPoint (&owner, Point<int>());
|
||||
|
||||
Rectangle<int> r (pos.x, pos.y, owner.getWidth(), owner.getHeight());
|
||||
r *= Desktop::getInstance().getDisplays().getPrimaryDisplay()->scale;
|
||||
|
||||
getEnv()->CallVoidMethod (view, AndroidView.layout, r.getX(), r.getY(),
|
||||
r.getRight(), r.getBottom());
|
||||
}
|
||||
}
|
||||
|
||||
void componentPeerChanged() override
|
||||
{
|
||||
auto* peer = owner.getPeer();
|
||||
|
||||
if (currentPeer != peer)
|
||||
{
|
||||
removeFromParent();
|
||||
currentPeer = peer;
|
||||
|
||||
addToParent();
|
||||
}
|
||||
|
||||
enum
|
||||
{
|
||||
VISIBLE = 0,
|
||||
INVISIBLE = 4
|
||||
};
|
||||
|
||||
getEnv()->CallVoidMethod (view, AndroidView.setVisibility, owner.isShowing() ? VISIBLE : INVISIBLE);
|
||||
}
|
||||
|
||||
void componentVisibilityChanged() override
|
||||
{
|
||||
componentPeerChanged();
|
||||
}
|
||||
|
||||
void componentBroughtToFront (Component& comp) override
|
||||
{
|
||||
ComponentMovementWatcher::componentBroughtToFront (comp);
|
||||
}
|
||||
|
||||
Rectangle<int> getViewBounds() const
|
||||
{
|
||||
auto* env = getEnv();
|
||||
|
||||
int width = env->CallIntMethod (view, AndroidView.getWidth);
|
||||
int height = env->CallIntMethod (view, AndroidView.getHeight);
|
||||
|
||||
return Rectangle<int> (width, height);
|
||||
}
|
||||
|
||||
GlobalRef view;
|
||||
|
||||
private:
|
||||
void addToParent()
|
||||
{
|
||||
if (currentPeer != nullptr)
|
||||
{
|
||||
jobject peerView = (jobject) currentPeer->getNativeHandle();
|
||||
|
||||
// NB: Assuming a parent is always of ViewGroup type
|
||||
auto* env = getEnv();
|
||||
|
||||
env->CallVoidMethod (peerView, AndroidViewGroup.addView, view.get());
|
||||
componentMovedOrResized (false, false);
|
||||
}
|
||||
}
|
||||
|
||||
void removeFromParent()
|
||||
{
|
||||
auto* env = getEnv();
|
||||
auto parentView = env->CallObjectMethod (view, AndroidView.getParent);
|
||||
|
||||
if (parentView != nullptr)
|
||||
{
|
||||
// Assuming a parent is always of ViewGroup type
|
||||
env->CallVoidMethod (parentView, AndroidViewGroup.removeView, view.get());
|
||||
}
|
||||
}
|
||||
|
||||
Component& owner;
|
||||
ComponentPeer* currentPeer = nullptr;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
AndroidViewComponent::AndroidViewComponent()
|
||||
{
|
||||
}
|
||||
|
||||
AndroidViewComponent::~AndroidViewComponent()
|
||||
{
|
||||
AccessibilityHandler::setNativeChildForComponent (*this, nullptr);
|
||||
}
|
||||
|
||||
void AndroidViewComponent::setView (void* view)
|
||||
{
|
||||
if (view != getView())
|
||||
{
|
||||
pimpl.reset();
|
||||
|
||||
if (view != nullptr)
|
||||
{
|
||||
// explicitly create a new local ref here so that we don't
|
||||
// delete the users pointer
|
||||
auto* env = getEnv();
|
||||
auto localref = LocalRef<jobject> (env->NewLocalRef ((jobject) view));
|
||||
|
||||
pimpl.reset (new Pimpl (localref, *this));
|
||||
|
||||
AccessibilityHandler::setNativeChildForComponent (*this, getView());
|
||||
}
|
||||
else
|
||||
{
|
||||
AccessibilityHandler::setNativeChildForComponent (*this, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void* AndroidViewComponent::getView() const
|
||||
{
|
||||
return pimpl == nullptr ? nullptr : (void*) pimpl->view;
|
||||
}
|
||||
|
||||
void AndroidViewComponent::resizeToFitView()
|
||||
{
|
||||
if (pimpl != nullptr)
|
||||
setBounds (pimpl->getViewBounds());
|
||||
}
|
||||
|
||||
void AndroidViewComponent::paint (Graphics&) {}
|
||||
|
||||
std::unique_ptr<AccessibilityHandler> AndroidViewComponent::createAccessibilityHandler()
|
||||
{
|
||||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::group);
|
||||
}
|
||||
|
||||
} // namespace juce
|
||||
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
AppleRemoteDevice::AppleRemoteDevice()
|
||||
: device (nullptr),
|
||||
queue (nullptr),
|
||||
remoteId (0)
|
||||
{
|
||||
}
|
||||
|
||||
AppleRemoteDevice::~AppleRemoteDevice()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
io_object_t getAppleRemoteDevice()
|
||||
{
|
||||
CFMutableDictionaryRef dict = IOServiceMatching ("AppleIRController");
|
||||
|
||||
io_iterator_t iter = 0;
|
||||
io_object_t iod = 0;
|
||||
|
||||
const auto defaultPort = []
|
||||
{
|
||||
#if JUCE_MAC_API_VERSION_CAN_BE_BUILT (12, 0)
|
||||
if (@available (macOS 12.0, *))
|
||||
return kIOMainPortDefault;
|
||||
#endif
|
||||
|
||||
JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS
|
||||
return kIOMasterPortDefault;
|
||||
JUCE_END_IGNORE_DEPRECATION_WARNINGS
|
||||
}();
|
||||
|
||||
if (IOServiceGetMatchingServices (defaultPort, dict, &iter) == kIOReturnSuccess
|
||||
&& iter != 0)
|
||||
{
|
||||
iod = IOIteratorNext (iter);
|
||||
}
|
||||
|
||||
IOObjectRelease (iter);
|
||||
return iod;
|
||||
}
|
||||
|
||||
bool createAppleRemoteInterface (io_object_t iod, void** device)
|
||||
{
|
||||
jassert (*device == nullptr);
|
||||
io_name_t classname;
|
||||
|
||||
if (IOObjectGetClass (iod, classname) == kIOReturnSuccess)
|
||||
{
|
||||
IOCFPlugInInterface** cfPlugInInterface = nullptr;
|
||||
SInt32 score = 0;
|
||||
|
||||
if (IOCreatePlugInInterfaceForService (iod,
|
||||
kIOHIDDeviceUserClientTypeID,
|
||||
kIOCFPlugInInterfaceID,
|
||||
&cfPlugInInterface,
|
||||
&score) == kIOReturnSuccess)
|
||||
{
|
||||
[[maybe_unused]] HRESULT hr = (*cfPlugInInterface)->QueryInterface (cfPlugInInterface,
|
||||
CFUUIDGetUUIDBytes (kIOHIDDeviceInterfaceID),
|
||||
device);
|
||||
|
||||
(*cfPlugInInterface)->Release (cfPlugInInterface);
|
||||
}
|
||||
}
|
||||
|
||||
return *device != nullptr;
|
||||
}
|
||||
|
||||
void appleRemoteQueueCallback (void* const target, const IOReturn result, void*, void*)
|
||||
{
|
||||
if (result == kIOReturnSuccess)
|
||||
((AppleRemoteDevice*) target)->handleCallbackInternal();
|
||||
}
|
||||
}
|
||||
|
||||
bool AppleRemoteDevice::start (const bool inExclusiveMode)
|
||||
{
|
||||
if (queue != nullptr)
|
||||
return true;
|
||||
|
||||
stop();
|
||||
|
||||
bool result = false;
|
||||
io_object_t iod = getAppleRemoteDevice();
|
||||
|
||||
if (iod != 0)
|
||||
{
|
||||
if (createAppleRemoteInterface (iod, &device) && open (inExclusiveMode))
|
||||
result = true;
|
||||
else
|
||||
stop();
|
||||
|
||||
IOObjectRelease (iod);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void AppleRemoteDevice::stop()
|
||||
{
|
||||
if (queue != nullptr)
|
||||
{
|
||||
(*(IOHIDQueueInterface**) queue)->stop ((IOHIDQueueInterface**) queue);
|
||||
(*(IOHIDQueueInterface**) queue)->dispose ((IOHIDQueueInterface**) queue);
|
||||
(*(IOHIDQueueInterface**) queue)->Release ((IOHIDQueueInterface**) queue);
|
||||
queue = nullptr;
|
||||
}
|
||||
|
||||
if (device != nullptr)
|
||||
{
|
||||
(*(IOHIDDeviceInterface**) device)->close ((IOHIDDeviceInterface**) device);
|
||||
(*(IOHIDDeviceInterface**) device)->Release ((IOHIDDeviceInterface**) device);
|
||||
device = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool AppleRemoteDevice::isActive() const
|
||||
{
|
||||
return queue != nullptr;
|
||||
}
|
||||
|
||||
bool AppleRemoteDevice::open (const bool openInExclusiveMode)
|
||||
{
|
||||
Array<int> cookies;
|
||||
|
||||
CFObjectHolder<CFArrayRef> elements;
|
||||
auto device122 = (IOHIDDeviceInterface122**) device;
|
||||
|
||||
if ((*device122)->copyMatchingElements (device122, nullptr, &elements.object) != kIOReturnSuccess)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < CFArrayGetCount (elements.object); ++i)
|
||||
{
|
||||
auto element = (CFDictionaryRef) CFArrayGetValueAtIndex (elements.object, i);
|
||||
|
||||
// get the cookie
|
||||
CFTypeRef object = CFDictionaryGetValue (element, CFSTR (kIOHIDElementCookieKey));
|
||||
|
||||
if (object == nullptr || CFGetTypeID (object) != CFNumberGetTypeID())
|
||||
continue;
|
||||
|
||||
long number;
|
||||
if (! CFNumberGetValue ((CFNumberRef) object, kCFNumberLongType, &number))
|
||||
continue;
|
||||
|
||||
cookies.add ((int) number);
|
||||
}
|
||||
|
||||
if ((*(IOHIDDeviceInterface**) device)
|
||||
->open ((IOHIDDeviceInterface**) device,
|
||||
openInExclusiveMode ? kIOHIDOptionsTypeSeizeDevice
|
||||
: kIOHIDOptionsTypeNone) == KERN_SUCCESS)
|
||||
{
|
||||
queue = (*(IOHIDDeviceInterface**) device)->allocQueue ((IOHIDDeviceInterface**) device);
|
||||
|
||||
if (queue != nullptr)
|
||||
{
|
||||
(*(IOHIDQueueInterface**) queue)->create ((IOHIDQueueInterface**) queue, 0, 12);
|
||||
|
||||
for (int i = 0; i < cookies.size(); ++i)
|
||||
{
|
||||
IOHIDElementCookie cookie = (IOHIDElementCookie) cookies.getUnchecked (i);
|
||||
(*(IOHIDQueueInterface**) queue)->addElement ((IOHIDQueueInterface**) queue, cookie, 0);
|
||||
}
|
||||
|
||||
CFRunLoopSourceRef eventSource;
|
||||
|
||||
if ((*(IOHIDQueueInterface**) queue)
|
||||
->createAsyncEventSource ((IOHIDQueueInterface**) queue, &eventSource) == KERN_SUCCESS)
|
||||
{
|
||||
if ((*(IOHIDQueueInterface**) queue)->setEventCallout ((IOHIDQueueInterface**) queue,
|
||||
appleRemoteQueueCallback, this, nullptr) == KERN_SUCCESS)
|
||||
{
|
||||
CFRunLoopAddSource (CFRunLoopGetCurrent(), eventSource, kCFRunLoopDefaultMode);
|
||||
|
||||
(*(IOHIDQueueInterface**) queue)->start ((IOHIDQueueInterface**) queue);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void AppleRemoteDevice::handleCallbackInternal()
|
||||
{
|
||||
int totalValues = 0;
|
||||
AbsoluteTime nullTime = { 0, 0 };
|
||||
char cookies [12];
|
||||
int numCookies = 0;
|
||||
|
||||
while (numCookies < numElementsInArray (cookies))
|
||||
{
|
||||
IOHIDEventStruct e;
|
||||
|
||||
if ((*(IOHIDQueueInterface**) queue)->getNextEvent ((IOHIDQueueInterface**) queue, &e, nullTime, 0) != kIOReturnSuccess)
|
||||
break;
|
||||
|
||||
if ((int) e.elementCookie == 19)
|
||||
{
|
||||
remoteId = e.value;
|
||||
buttonPressed (switched, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
totalValues += e.value;
|
||||
cookies [numCookies++] = (char) (pointer_sized_int) e.elementCookie;
|
||||
}
|
||||
}
|
||||
|
||||
cookies [numCookies++] = 0;
|
||||
|
||||
static const char buttonPatterns[] =
|
||||
{
|
||||
0x1f, 0x14, 0x12, 0x1f, 0x14, 0x12, 0,
|
||||
0x1f, 0x15, 0x12, 0x1f, 0x15, 0x12, 0,
|
||||
0x1f, 0x1d, 0x1c, 0x12, 0,
|
||||
0x1f, 0x1e, 0x1c, 0x12, 0,
|
||||
0x1f, 0x16, 0x12, 0x1f, 0x16, 0x12, 0,
|
||||
0x1f, 0x17, 0x12, 0x1f, 0x17, 0x12, 0,
|
||||
0x1f, 0x12, 0x04, 0x02, 0,
|
||||
0x1f, 0x12, 0x03, 0x02, 0,
|
||||
0x1f, 0x12, 0x1f, 0x12, 0,
|
||||
0x23, 0x1f, 0x12, 0x23, 0x1f, 0x12, 0,
|
||||
19, 0
|
||||
};
|
||||
|
||||
int buttonNum = (int) menuButton;
|
||||
int i = 0;
|
||||
|
||||
while (i < numElementsInArray (buttonPatterns))
|
||||
{
|
||||
if (strcmp (cookies, buttonPatterns + i) == 0)
|
||||
{
|
||||
buttonPressed ((ButtonType) buttonNum, totalValues > 0);
|
||||
break;
|
||||
}
|
||||
|
||||
i += (int) strlen (buttonPatterns + i) + 1;
|
||||
++buttonNum;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace juce
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
class HWNDComponent::Pimpl : public ComponentMovementWatcher
|
||||
{
|
||||
public:
|
||||
Pimpl (HWND h, Component& comp)
|
||||
: ComponentMovementWatcher (&comp),
|
||||
hwnd (h),
|
||||
owner (comp)
|
||||
{
|
||||
if (owner.isShowing())
|
||||
componentPeerChanged();
|
||||
}
|
||||
|
||||
~Pimpl() override
|
||||
{
|
||||
removeFromParent();
|
||||
DestroyWindow (hwnd);
|
||||
}
|
||||
|
||||
void componentMovedOrResized (bool wasMoved, bool wasResized) override
|
||||
{
|
||||
if (auto* peer = owner.getTopLevelComponent()->getPeer())
|
||||
{
|
||||
auto area = (peer->getAreaCoveredBy (owner).toFloat() * peer->getPlatformScaleFactor()).getSmallestIntegerContainer();
|
||||
|
||||
UINT flagsToSend = SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOOWNERZORDER;
|
||||
|
||||
if (! wasMoved) flagsToSend |= SWP_NOMOVE;
|
||||
if (! wasResized) flagsToSend |= SWP_NOSIZE;
|
||||
|
||||
ScopedThreadDPIAwarenessSetter threadDpiAwarenessSetter { hwnd };
|
||||
|
||||
SetWindowPos (hwnd, nullptr, area.getX(), area.getY(), area.getWidth(), area.getHeight(), flagsToSend);
|
||||
|
||||
invalidateHWNDAndChildren();
|
||||
}
|
||||
}
|
||||
|
||||
using ComponentMovementWatcher::componentMovedOrResized;
|
||||
|
||||
void componentPeerChanged() override
|
||||
{
|
||||
auto* peer = owner.getPeer();
|
||||
|
||||
if (currentPeer != peer)
|
||||
{
|
||||
removeFromParent();
|
||||
currentPeer = peer;
|
||||
|
||||
addToParent();
|
||||
}
|
||||
|
||||
auto isShowing = owner.isShowing();
|
||||
|
||||
ShowWindow (hwnd, isShowing ? SW_SHOWNA : SW_HIDE);
|
||||
|
||||
if (isShowing)
|
||||
InvalidateRect (hwnd, nullptr, TRUE);
|
||||
}
|
||||
|
||||
void componentVisibilityChanged() override
|
||||
{
|
||||
componentPeerChanged();
|
||||
}
|
||||
|
||||
using ComponentMovementWatcher::componentVisibilityChanged;
|
||||
|
||||
void componentBroughtToFront (Component& comp) override
|
||||
{
|
||||
ComponentMovementWatcher::componentBroughtToFront (comp);
|
||||
}
|
||||
|
||||
Rectangle<int> getHWNDBounds() const
|
||||
{
|
||||
if (auto* peer = owner.getPeer())
|
||||
{
|
||||
ScopedThreadDPIAwarenessSetter threadDpiAwarenessSetter { hwnd };
|
||||
|
||||
RECT r;
|
||||
GetWindowRect (hwnd, &r);
|
||||
Rectangle<int> windowRectangle (r.right - r.left, r.bottom - r.top);
|
||||
|
||||
return (windowRectangle.toFloat() / peer->getPlatformScaleFactor()).toNearestInt();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void invalidateHWNDAndChildren()
|
||||
{
|
||||
EnumChildWindows (hwnd, invalidateHwndCallback, 0);
|
||||
}
|
||||
|
||||
static BOOL WINAPI invalidateHwndCallback (HWND hwnd, LPARAM)
|
||||
{
|
||||
InvalidateRect (hwnd, nullptr, TRUE);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
HWND hwnd;
|
||||
|
||||
private:
|
||||
void addToParent()
|
||||
{
|
||||
if (currentPeer != nullptr)
|
||||
{
|
||||
auto windowFlags = GetWindowLongPtr (hwnd, GWL_STYLE);
|
||||
|
||||
using FlagType = decltype (windowFlags);
|
||||
|
||||
windowFlags &= ~(FlagType) WS_POPUP;
|
||||
windowFlags |= (FlagType) WS_CHILD;
|
||||
|
||||
SetWindowLongPtr (hwnd, GWL_STYLE, windowFlags);
|
||||
SetParent (hwnd, (HWND) currentPeer->getNativeHandle());
|
||||
|
||||
componentMovedOrResized (true, true);
|
||||
}
|
||||
}
|
||||
|
||||
void removeFromParent()
|
||||
{
|
||||
ShowWindow (hwnd, SW_HIDE);
|
||||
SetParent (hwnd, nullptr);
|
||||
}
|
||||
|
||||
Component& owner;
|
||||
ComponentPeer* currentPeer = nullptr;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
HWNDComponent::HWNDComponent() {}
|
||||
HWNDComponent::~HWNDComponent() {}
|
||||
|
||||
void HWNDComponent::paint (Graphics&) {}
|
||||
|
||||
void HWNDComponent::setHWND (void* hwnd)
|
||||
{
|
||||
if (hwnd != getHWND())
|
||||
{
|
||||
pimpl.reset();
|
||||
|
||||
if (hwnd != nullptr)
|
||||
pimpl.reset (new Pimpl ((HWND) hwnd, *this));
|
||||
}
|
||||
}
|
||||
|
||||
void* HWNDComponent::getHWND() const
|
||||
{
|
||||
return pimpl == nullptr ? nullptr : (void*) pimpl->hwnd;
|
||||
}
|
||||
|
||||
void HWNDComponent::resizeToFit()
|
||||
{
|
||||
if (pimpl != nullptr)
|
||||
setBounds (pimpl->getHWNDBounds());
|
||||
}
|
||||
|
||||
void HWNDComponent::updateHWNDBounds()
|
||||
{
|
||||
if (pimpl != nullptr)
|
||||
pimpl->componentMovedOrResized (true, true);
|
||||
}
|
||||
|
||||
} // namespace juce
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
class NSViewAttachment final : public ReferenceCountedObject,
|
||||
public ComponentMovementWatcher
|
||||
{
|
||||
public:
|
||||
NSViewAttachment (NSView* v, Component& comp)
|
||||
: ComponentMovementWatcher (&comp),
|
||||
view (v), owner (comp)
|
||||
{
|
||||
[view retain];
|
||||
[view setPostsFrameChangedNotifications: YES];
|
||||
updateAlpha();
|
||||
|
||||
if (owner.isShowing())
|
||||
componentPeerChanged();
|
||||
}
|
||||
|
||||
~NSViewAttachment() override
|
||||
{
|
||||
removeFromParent();
|
||||
[view release];
|
||||
}
|
||||
|
||||
void componentMovedOrResized (Component& comp, bool wasMoved, bool wasResized) override
|
||||
{
|
||||
ComponentMovementWatcher::componentMovedOrResized (comp, wasMoved, wasResized);
|
||||
|
||||
// The ComponentMovementWatcher version of this method avoids calling
|
||||
// us when the top-level comp is resized, but if we're listening to the
|
||||
// top-level comp we still want the NSView to track its size.
|
||||
if (comp.isOnDesktop() && wasResized)
|
||||
componentMovedOrResized (wasMoved, wasResized);
|
||||
}
|
||||
|
||||
void componentMovedOrResized (bool /*wasMoved*/, bool /*wasResized*/) override
|
||||
{
|
||||
if (auto* peer = owner.getTopLevelComponent()->getPeer())
|
||||
{
|
||||
const auto newArea = peer->getAreaCoveredBy (owner);
|
||||
|
||||
if (convertToRectInt ([view frame]) != newArea)
|
||||
[view setFrame: makeCGRect (newArea)];
|
||||
}
|
||||
}
|
||||
|
||||
void componentPeerChanged() override
|
||||
{
|
||||
auto* peer = owner.getPeer();
|
||||
|
||||
if (std::exchange (currentPeer, peer) != peer)
|
||||
{
|
||||
if (peer != nullptr)
|
||||
{
|
||||
auto peerView = (NSView*) peer->getNativeHandle();
|
||||
[peerView addSubview: view];
|
||||
|
||||
componentMovedOrResized (false, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
removeFromParent();
|
||||
}
|
||||
}
|
||||
|
||||
[view setHidden: ! owner.isShowing()];
|
||||
}
|
||||
|
||||
void componentVisibilityChanged() override
|
||||
{
|
||||
componentPeerChanged();
|
||||
}
|
||||
|
||||
void updateAlpha()
|
||||
{
|
||||
[view setAlphaValue: (CGFloat) owner.getAlpha()];
|
||||
}
|
||||
|
||||
NSView* const view;
|
||||
|
||||
using Ptr = ReferenceCountedObjectPtr<NSViewAttachment>;
|
||||
|
||||
private:
|
||||
Component& owner;
|
||||
ComponentPeer* currentPeer = nullptr;
|
||||
NSViewFrameWatcher frameWatcher { view, [this] { owner.childBoundsChanged (nullptr); } };
|
||||
|
||||
void removeFromParent()
|
||||
{
|
||||
if ([view superview] != nil)
|
||||
[view removeFromSuperview]; // Must be careful not to call this unless it's required - e.g. some Apple AU views
|
||||
// override the call and use it as a sign that they're being deleted, which breaks everything..
|
||||
}
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NSViewAttachment)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
NSViewComponent::NSViewComponent() = default;
|
||||
NSViewComponent::~NSViewComponent()
|
||||
{
|
||||
AccessibilityHandler::setNativeChildForComponent (*this, nullptr);
|
||||
}
|
||||
|
||||
void NSViewComponent::setView (void* view)
|
||||
{
|
||||
if (view != getView())
|
||||
{
|
||||
auto old = attachment;
|
||||
|
||||
attachment = nullptr;
|
||||
|
||||
if (view != nullptr)
|
||||
attachment = attachViewToComponent (*this, view);
|
||||
|
||||
old = nullptr;
|
||||
|
||||
AccessibilityHandler::setNativeChildForComponent (*this, getView());
|
||||
}
|
||||
}
|
||||
|
||||
void* NSViewComponent::getView() const
|
||||
{
|
||||
return attachment != nullptr ? static_cast<NSViewAttachment*> (attachment.get())->view
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
void NSViewComponent::resizeToFitView()
|
||||
{
|
||||
if (attachment != nullptr)
|
||||
{
|
||||
auto* view = static_cast<NSViewAttachment*> (attachment.get())->view;
|
||||
auto r = [view frame];
|
||||
setBounds (Rectangle<int> ((int) r.size.width, (int) r.size.height));
|
||||
|
||||
if (auto* peer = getTopLevelComponent()->getPeer())
|
||||
{
|
||||
const auto position = peer->getAreaCoveredBy (*this).getPosition();
|
||||
[view setFrameOrigin: convertToCGPoint (position)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NSViewComponent::resizeViewToFit()
|
||||
{
|
||||
if (attachment != nullptr)
|
||||
static_cast<NSViewAttachment*> (attachment.get())->componentMovedOrResized (true, true);
|
||||
}
|
||||
|
||||
void NSViewComponent::paint (Graphics&) {}
|
||||
|
||||
void NSViewComponent::alphaChanged()
|
||||
{
|
||||
if (attachment != nullptr)
|
||||
(static_cast<NSViewAttachment*> (attachment.get()))->updateAlpha();
|
||||
}
|
||||
|
||||
ReferenceCountedObject* NSViewComponent::attachViewToComponent (Component& comp, void* view)
|
||||
{
|
||||
return new NSViewAttachment ((NSView*) view, comp);
|
||||
}
|
||||
|
||||
std::unique_ptr<AccessibilityHandler> NSViewComponent::createAccessibilityHandler()
|
||||
{
|
||||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::group);
|
||||
}
|
||||
|
||||
} // namespace juce
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#if JUCE_MAC
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
|
||||
const auto nsViewFrameChangedSelector = @selector (frameChanged:);
|
||||
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||||
|
||||
struct NSViewCallbackInterface
|
||||
{
|
||||
virtual ~NSViewCallbackInterface() = default;
|
||||
virtual void frameChanged() = 0;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
struct NSViewFrameChangeCallbackClass : public ObjCClass<NSObject>
|
||||
{
|
||||
NSViewFrameChangeCallbackClass()
|
||||
: ObjCClass ("JUCE_NSViewCallback_")
|
||||
{
|
||||
addIvar<NSViewCallbackInterface*> ("target");
|
||||
|
||||
addMethod (nsViewFrameChangedSelector, frameChanged);
|
||||
|
||||
registerClass();
|
||||
}
|
||||
|
||||
static void setTarget (id self, NSViewCallbackInterface* c)
|
||||
{
|
||||
object_setInstanceVariable (self, "target", c);
|
||||
}
|
||||
|
||||
private:
|
||||
static void frameChanged (id self, SEL, NSNotification*)
|
||||
{
|
||||
if (auto* target = getIvar<NSViewCallbackInterface*> (self, "target"))
|
||||
target->frameChanged();
|
||||
}
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE (NSViewFrameChangeCallbackClass)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class NSViewFrameWatcher : private NSViewCallbackInterface
|
||||
{
|
||||
public:
|
||||
NSViewFrameWatcher (NSView* viewToWatch, std::function<void()> viewResizedIn)
|
||||
: viewResized (std::move (viewResizedIn)), callback (makeCallbackForView (viewToWatch))
|
||||
{
|
||||
}
|
||||
|
||||
~NSViewFrameWatcher() override
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver: callback];
|
||||
[callback release];
|
||||
callback = nil;
|
||||
}
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE (NSViewFrameWatcher)
|
||||
JUCE_DECLARE_NON_MOVEABLE (NSViewFrameWatcher)
|
||||
|
||||
private:
|
||||
id makeCallbackForView (NSView* view)
|
||||
{
|
||||
static NSViewFrameChangeCallbackClass cls;
|
||||
auto* result = [cls.createInstance() init];
|
||||
NSViewFrameChangeCallbackClass::setTarget (result, this);
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver: result
|
||||
selector: nsViewFrameChangedSelector
|
||||
name: NSViewFrameDidChangeNotification
|
||||
object: view];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void frameChanged() override { viewResized(); }
|
||||
|
||||
std::function<void()> viewResized;
|
||||
id callback;
|
||||
};
|
||||
|
||||
} // namespace juce
|
||||
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,622 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
#define JUCE_PUSH_NOTIFICATIONS_IMPL 1
|
||||
|
||||
struct PushNotificationsDelegateDetails
|
||||
{
|
||||
//==============================================================================
|
||||
using Action = PushNotifications::Settings::Action;
|
||||
using Category = PushNotifications::Settings::Category;
|
||||
|
||||
static void* actionToNSAction (const Action& a)
|
||||
{
|
||||
if (a.style == Action::text)
|
||||
{
|
||||
return [UNTextInputNotificationAction actionWithIdentifier: juceStringToNS (a.identifier)
|
||||
title: juceStringToNS (a.title)
|
||||
options: NSUInteger (a.destructive << 1 | (! a.triggerInBackground) << 2)
|
||||
textInputButtonTitle: juceStringToNS (a.textInputButtonText)
|
||||
textInputPlaceholder: juceStringToNS (a.textInputPlaceholder)];
|
||||
}
|
||||
|
||||
return [UNNotificationAction actionWithIdentifier: juceStringToNS (a.identifier)
|
||||
title: juceStringToNS (a.title)
|
||||
options: NSUInteger (a.destructive << 1 | (! a.triggerInBackground) << 2)];
|
||||
}
|
||||
|
||||
static void* categoryToNSCategory (const Category& c)
|
||||
{
|
||||
auto actions = [NSMutableArray arrayWithCapacity: (NSUInteger) c.actions.size()];
|
||||
|
||||
for (const auto& a : c.actions)
|
||||
{
|
||||
auto* action = (UNNotificationAction*) actionToNSAction (a);
|
||||
[actions addObject: action];
|
||||
}
|
||||
|
||||
return [UNNotificationCategory categoryWithIdentifier: juceStringToNS (c.identifier)
|
||||
actions: actions
|
||||
intentIdentifiers: @[]
|
||||
options: c.sendDismissAction ? UNNotificationCategoryOptionCustomDismissAction : 0];
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static UNNotificationRequest* juceNotificationToUNNotificationRequest (const PushNotifications::Notification& n)
|
||||
{
|
||||
// content
|
||||
auto content = [[UNMutableNotificationContent alloc] init];
|
||||
|
||||
content.title = juceStringToNS (n.title);
|
||||
content.subtitle = juceStringToNS (n.subtitle);
|
||||
content.threadIdentifier = juceStringToNS (n.groupId);
|
||||
content.body = juceStringToNS (n.body);
|
||||
content.categoryIdentifier = juceStringToNS (n.category);
|
||||
content.badge = [NSNumber numberWithInt: n.badgeNumber];
|
||||
|
||||
auto soundToPlayString = n.soundToPlay.toString (true);
|
||||
|
||||
if (soundToPlayString == "default_os_sound")
|
||||
content.sound = [UNNotificationSound defaultSound];
|
||||
else if (soundToPlayString.isNotEmpty())
|
||||
content.sound = [UNNotificationSound soundNamed: juceStringToNS (soundToPlayString)];
|
||||
|
||||
auto* propsDict = (NSMutableDictionary*) [varToNSDictionary (n.properties) mutableCopy];
|
||||
[propsDict setObject: juceStringToNS (soundToPlayString) forKey: @"com.juce.soundName"];
|
||||
content.userInfo = propsDict;
|
||||
|
||||
// trigger
|
||||
UNTimeIntervalNotificationTrigger* trigger = nil;
|
||||
|
||||
if (std::abs (n.triggerIntervalSec) >= 0.001)
|
||||
{
|
||||
BOOL shouldRepeat = n.repeat && n.triggerIntervalSec >= 60;
|
||||
trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval: n.triggerIntervalSec repeats: shouldRepeat];
|
||||
}
|
||||
|
||||
// request
|
||||
// each notification needs to have an identifier, otherwise it will not show up
|
||||
jassert (n.identifier.isNotEmpty());
|
||||
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier: juceStringToNS (n.identifier)
|
||||
content: content
|
||||
trigger: trigger];
|
||||
|
||||
[content autorelease];
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
static String getUserResponseFromNSDictionary (NSDictionary* dictionary)
|
||||
{
|
||||
if (dictionary == nil || dictionary.count == 0)
|
||||
return {};
|
||||
|
||||
jassert (dictionary.count == 1);
|
||||
|
||||
for (NSString* key in dictionary)
|
||||
{
|
||||
const auto keyString = nsStringToJuce (key);
|
||||
|
||||
id value = dictionary[key];
|
||||
|
||||
if ([value isKindOfClass: [NSString class]])
|
||||
return nsStringToJuce ((NSString*) value);
|
||||
}
|
||||
|
||||
jassertfalse;
|
||||
return {};
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static var getNotificationPropertiesFromDictionaryVar (const var& dictionaryVar)
|
||||
{
|
||||
DynamicObject* dictionaryVarObject = dictionaryVar.getDynamicObject();
|
||||
|
||||
if (dictionaryVarObject == nullptr)
|
||||
return {};
|
||||
|
||||
const auto& properties = dictionaryVarObject->getProperties();
|
||||
|
||||
DynamicObject::Ptr propsVarObject = new DynamicObject();
|
||||
|
||||
for (int i = 0; i < properties.size(); ++i)
|
||||
{
|
||||
auto propertyName = properties.getName (i).toString();
|
||||
|
||||
if (propertyName == "aps")
|
||||
continue;
|
||||
|
||||
propsVarObject->setProperty (propertyName, properties.getValueAt (i));
|
||||
}
|
||||
|
||||
return var (propsVarObject.get());
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static double getIntervalSecFromUNNotificationTrigger (UNNotificationTrigger* t)
|
||||
{
|
||||
if (t != nil)
|
||||
{
|
||||
if ([t isKindOfClass: [UNTimeIntervalNotificationTrigger class]])
|
||||
{
|
||||
auto* trigger = (UNTimeIntervalNotificationTrigger*) t;
|
||||
return trigger.timeInterval;
|
||||
}
|
||||
else if ([t isKindOfClass: [UNCalendarNotificationTrigger class]])
|
||||
{
|
||||
auto* trigger = (UNCalendarNotificationTrigger*) t;
|
||||
NSDate* date = [trigger.dateComponents date];
|
||||
NSDate* dateNow = [NSDate date];
|
||||
return [dateNow timeIntervalSinceDate: date];
|
||||
}
|
||||
}
|
||||
|
||||
return 0.;
|
||||
}
|
||||
|
||||
static PushNotifications::Notification unNotificationRequestToJuceNotification (UNNotificationRequest* r)
|
||||
{
|
||||
PushNotifications::Notification n;
|
||||
|
||||
n.identifier = nsStringToJuce (r.identifier);
|
||||
n.title = nsStringToJuce (r.content.title);
|
||||
n.subtitle = nsStringToJuce (r.content.subtitle);
|
||||
n.body = nsStringToJuce (r.content.body);
|
||||
n.groupId = nsStringToJuce (r.content.threadIdentifier);
|
||||
n.category = nsStringToJuce (r.content.categoryIdentifier);
|
||||
n.badgeNumber = r.content.badge.intValue;
|
||||
|
||||
auto userInfoVar = nsDictionaryToVar (r.content.userInfo);
|
||||
|
||||
if (auto* object = userInfoVar.getDynamicObject())
|
||||
{
|
||||
static const Identifier soundName ("com.juce.soundName");
|
||||
n.soundToPlay = URL (object->getProperty (soundName).toString());
|
||||
object->removeProperty (soundName);
|
||||
}
|
||||
|
||||
n.properties = userInfoVar;
|
||||
|
||||
n.triggerIntervalSec = getIntervalSecFromUNNotificationTrigger (r.trigger);
|
||||
n.repeat = r.trigger != nil && r.trigger.repeats;
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
static PushNotifications::Notification unNotificationToJuceNotification (UNNotification* n)
|
||||
{
|
||||
return unNotificationRequestToJuceNotification (n.request);
|
||||
}
|
||||
|
||||
static Action unNotificationActionToAction (UNNotificationAction* a)
|
||||
{
|
||||
Action action;
|
||||
|
||||
action.identifier = nsStringToJuce (a.identifier);
|
||||
action.title = nsStringToJuce (a.title);
|
||||
action.triggerInBackground = ! (a.options & UNNotificationActionOptionForeground);
|
||||
action.destructive = a.options & UNNotificationActionOptionDestructive;
|
||||
|
||||
if ([a isKindOfClass: [UNTextInputNotificationAction class]])
|
||||
{
|
||||
auto* textAction = (UNTextInputNotificationAction*)a;
|
||||
|
||||
action.style = Action::text;
|
||||
action.textInputButtonText = nsStringToJuce (textAction.textInputButtonTitle);
|
||||
action.textInputPlaceholder = nsStringToJuce (textAction.textInputPlaceholder);
|
||||
}
|
||||
else
|
||||
{
|
||||
action.style = Action::button;
|
||||
}
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
static Category unNotificationCategoryToCategory (UNNotificationCategory* c)
|
||||
{
|
||||
Category category;
|
||||
|
||||
category.identifier = nsStringToJuce (c.identifier);
|
||||
category.sendDismissAction = c.options & UNNotificationCategoryOptionCustomDismissAction;
|
||||
|
||||
for (UNNotificationAction* a in c.actions)
|
||||
category.actions.add (unNotificationActionToAction (a));
|
||||
|
||||
return category;
|
||||
}
|
||||
|
||||
static PushNotifications::Notification nsDictionaryToJuceNotification (NSDictionary* dictionary)
|
||||
{
|
||||
const var dictionaryVar = nsDictionaryToVar (dictionary);
|
||||
|
||||
const var apsVar = dictionaryVar.getProperty ("aps", {});
|
||||
|
||||
if (! apsVar.isObject())
|
||||
return {};
|
||||
|
||||
var alertVar = apsVar.getProperty ("alert", {});
|
||||
|
||||
const var titleVar = alertVar.getProperty ("title", {});
|
||||
const var bodyVar = alertVar.isObject() ? alertVar.getProperty ("body", {}) : alertVar;
|
||||
|
||||
const var categoryVar = apsVar.getProperty ("category", {});
|
||||
const var soundVar = apsVar.getProperty ("sound", {});
|
||||
const var badgeVar = apsVar.getProperty ("badge", {});
|
||||
const var threadIdVar = apsVar.getProperty ("thread-id", {});
|
||||
|
||||
PushNotifications::Notification notification;
|
||||
|
||||
notification.title = titleVar .toString();
|
||||
notification.body = bodyVar .toString();
|
||||
notification.groupId = threadIdVar.toString();
|
||||
notification.category = categoryVar.toString();
|
||||
notification.soundToPlay = URL (soundVar.toString());
|
||||
notification.badgeNumber = (int) badgeVar;
|
||||
notification.properties = getNotificationPropertiesFromDictionaryVar (dictionaryVar);
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
private:
|
||||
~PushNotificationsDelegateDetails() = delete;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
bool PushNotifications::Notification::isValid() const noexcept
|
||||
{
|
||||
return title.isNotEmpty() && body.isNotEmpty() && identifier.isNotEmpty() && category.isNotEmpty();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
struct PushNotifications::Impl
|
||||
{
|
||||
explicit Impl (PushNotifications& p)
|
||||
: owner (p)
|
||||
{
|
||||
Class::setThis (delegate.get(), this);
|
||||
|
||||
auto appDelegate = [[UIApplication sharedApplication] delegate];
|
||||
|
||||
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
|
||||
if ([appDelegate respondsToSelector: @selector (setPushNotificationsDelegateToUse:)])
|
||||
[appDelegate performSelector: @selector (setPushNotificationsDelegateToUse:) withObject: delegate.get()];
|
||||
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||||
}
|
||||
|
||||
void requestPermissionsWithSettings (const PushNotifications::Settings& settingsToUse)
|
||||
{
|
||||
settings = settingsToUse;
|
||||
|
||||
auto categories = [NSMutableSet setWithCapacity: (NSUInteger) settings.categories.size()];
|
||||
|
||||
for (const auto& c : settings.categories)
|
||||
{
|
||||
auto* category = (UNNotificationCategory*) PushNotificationsDelegateDetails::categoryToNSCategory (c);
|
||||
[categories addObject: category];
|
||||
}
|
||||
|
||||
UNAuthorizationOptions authOptions = NSUInteger ((bool)settings.allowBadge << 0
|
||||
| (bool)settings.allowSound << 1
|
||||
| (bool)settings.allowAlert << 2);
|
||||
|
||||
[[UNUserNotificationCenter currentNotificationCenter] setNotificationCategories: categories];
|
||||
[[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions: authOptions
|
||||
completionHandler: ^(BOOL /*granted*/, NSError* /*error*/)
|
||||
{
|
||||
requestSettingsUsed();
|
||||
}];
|
||||
|
||||
[[UIApplication sharedApplication] registerForRemoteNotifications];
|
||||
}
|
||||
|
||||
void requestSettingsUsed()
|
||||
{
|
||||
[[UNUserNotificationCenter currentNotificationCenter] getNotificationSettingsWithCompletionHandler:
|
||||
^(UNNotificationSettings* s)
|
||||
{
|
||||
[[UNUserNotificationCenter currentNotificationCenter] getNotificationCategoriesWithCompletionHandler:
|
||||
^(NSSet<UNNotificationCategory*>* categories)
|
||||
{
|
||||
settings.allowBadge = s.badgeSetting == UNNotificationSettingEnabled;
|
||||
settings.allowSound = s.soundSetting == UNNotificationSettingEnabled;
|
||||
settings.allowAlert = s.alertSetting == UNNotificationSettingEnabled;
|
||||
|
||||
for (UNNotificationCategory* c in categories)
|
||||
settings.categories.add (PushNotificationsDelegateDetails::unNotificationCategoryToCategory (c));
|
||||
|
||||
owner.listeners.call ([&] (Listener& l) { l.notificationSettingsReceived (settings); });
|
||||
}
|
||||
];
|
||||
}];
|
||||
}
|
||||
|
||||
bool areNotificationsEnabled() const { return true; }
|
||||
|
||||
void sendLocalNotification (const Notification& n)
|
||||
{
|
||||
UNNotificationRequest* request = PushNotificationsDelegateDetails::juceNotificationToUNNotificationRequest (n);
|
||||
|
||||
[[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest: request
|
||||
withCompletionHandler: ^(NSError* error)
|
||||
{
|
||||
jassert (error == nil);
|
||||
|
||||
if (error != nil)
|
||||
NSLog (nsStringLiteral ("addNotificationRequest error: %@"), error);
|
||||
}];
|
||||
}
|
||||
|
||||
void getDeliveredNotifications() const
|
||||
{
|
||||
[[UNUserNotificationCenter currentNotificationCenter] getDeliveredNotificationsWithCompletionHandler:
|
||||
^(NSArray<UNNotification*>* notifications)
|
||||
{
|
||||
Array<PushNotifications::Notification> notifs;
|
||||
|
||||
for (UNNotification* n in notifications)
|
||||
notifs.add (PushNotificationsDelegateDetails::unNotificationToJuceNotification (n));
|
||||
|
||||
owner.listeners.call ([&] (Listener& l) { l.deliveredNotificationsListReceived (notifs); });
|
||||
}];
|
||||
}
|
||||
|
||||
void removeAllDeliveredNotifications()
|
||||
{
|
||||
[[UNUserNotificationCenter currentNotificationCenter] removeAllDeliveredNotifications];
|
||||
}
|
||||
|
||||
void removeDeliveredNotification ([[maybe_unused]] const String& identifier)
|
||||
{
|
||||
NSArray<NSString*>* identifiers = [NSArray arrayWithObject: juceStringToNS (identifier)];
|
||||
|
||||
[[UNUserNotificationCenter currentNotificationCenter] removeDeliveredNotificationsWithIdentifiers: identifiers];
|
||||
}
|
||||
|
||||
void setupChannels ([[maybe_unused]] const Array<ChannelGroup>& groups, [[maybe_unused]] const Array<Channel>& channels)
|
||||
{
|
||||
}
|
||||
|
||||
void getPendingLocalNotifications() const
|
||||
{
|
||||
[[UNUserNotificationCenter currentNotificationCenter] getPendingNotificationRequestsWithCompletionHandler:
|
||||
^(NSArray<UNNotificationRequest*>* requests)
|
||||
{
|
||||
Array<PushNotifications::Notification> notifs;
|
||||
|
||||
for (UNNotificationRequest* r : requests)
|
||||
notifs.add (PushNotificationsDelegateDetails::unNotificationRequestToJuceNotification (r));
|
||||
|
||||
owner.listeners.call ([&] (Listener& l) { l.pendingLocalNotificationsListReceived (notifs); });
|
||||
}];
|
||||
}
|
||||
|
||||
void removePendingLocalNotification (const String& identifier)
|
||||
{
|
||||
NSArray<NSString*>* identifiers = [NSArray arrayWithObject: juceStringToNS (identifier)];
|
||||
[[UNUserNotificationCenter currentNotificationCenter] removePendingNotificationRequestsWithIdentifiers: identifiers];
|
||||
}
|
||||
|
||||
void removeAllPendingLocalNotifications()
|
||||
{
|
||||
[[UNUserNotificationCenter currentNotificationCenter] removeAllPendingNotificationRequests];
|
||||
}
|
||||
|
||||
String getDeviceToken()
|
||||
{
|
||||
// You need to call requestPermissionsWithSettings() first.
|
||||
jassert (initialised);
|
||||
|
||||
return deviceToken;
|
||||
}
|
||||
|
||||
void subscribeToTopic ([[maybe_unused]] const String& topic) {}
|
||||
void unsubscribeFromTopic ([[maybe_unused]] const String& topic) {}
|
||||
|
||||
void sendUpstreamMessage ([[maybe_unused]] const String& serverSenderId,
|
||||
[[maybe_unused]] const String& collapseKey,
|
||||
[[maybe_unused]] const String& messageId,
|
||||
[[maybe_unused]] const String& messageType,
|
||||
[[maybe_unused]] int timeToLive,
|
||||
[[maybe_unused]] const StringPairArray& additionalData)
|
||||
{
|
||||
}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
void registeredForRemoteNotifications (NSData* deviceTokenToUse)
|
||||
{
|
||||
deviceToken = [deviceTokenToUse]() -> String
|
||||
{
|
||||
auto length = deviceTokenToUse.length;
|
||||
|
||||
if (auto* buffer = (const unsigned char*) deviceTokenToUse.bytes)
|
||||
{
|
||||
NSMutableString* hexString = [NSMutableString stringWithCapacity: (length * 2)];
|
||||
|
||||
for (NSUInteger i = 0; i < length; ++i)
|
||||
[hexString appendFormat:@"%02x", buffer[i]];
|
||||
|
||||
return nsStringToJuce ([hexString copy]);
|
||||
}
|
||||
|
||||
return {};
|
||||
}();
|
||||
|
||||
initialised = true;
|
||||
|
||||
owner.listeners.call ([&] (Listener& l) { l.deviceTokenRefreshed (deviceToken); });
|
||||
}
|
||||
|
||||
void failedToRegisterForRemoteNotifications ([[maybe_unused]] NSError* error)
|
||||
{
|
||||
deviceToken.clear();
|
||||
}
|
||||
|
||||
void didReceiveRemoteNotification (NSDictionary* userInfo)
|
||||
{
|
||||
auto n = PushNotificationsDelegateDetails::nsDictionaryToJuceNotification (userInfo);
|
||||
|
||||
owner.listeners.call ([&] (Listener& l) { l.handleNotification (false, n); });
|
||||
}
|
||||
|
||||
void didReceiveRemoteNotificationFetchCompletionHandler (NSDictionary* userInfo,
|
||||
void (^completionHandler)(UIBackgroundFetchResult result))
|
||||
{
|
||||
didReceiveRemoteNotification (userInfo);
|
||||
completionHandler (UIBackgroundFetchResultNewData);
|
||||
}
|
||||
|
||||
void handleActionForRemoteNotificationCompletionHandler (NSString* actionIdentifier,
|
||||
NSDictionary* userInfo,
|
||||
NSDictionary* responseInfo,
|
||||
void (^completionHandler)())
|
||||
{
|
||||
auto n = PushNotificationsDelegateDetails::nsDictionaryToJuceNotification (userInfo);
|
||||
auto actionString = nsStringToJuce (actionIdentifier);
|
||||
auto response = PushNotificationsDelegateDetails::getUserResponseFromNSDictionary (responseInfo);
|
||||
|
||||
owner.listeners.call ([&] (Listener& l) { l.handleNotificationAction (false, n, actionString, response); });
|
||||
|
||||
completionHandler();
|
||||
}
|
||||
|
||||
void willPresentNotificationWithCompletionHandler ([[maybe_unused]] UNNotification* notification,
|
||||
void (^completionHandler)(UNNotificationPresentationOptions options))
|
||||
{
|
||||
NSUInteger options = NSUInteger ((int)settings.allowBadge << 0
|
||||
| (int)settings.allowSound << 1
|
||||
| (int)settings.allowAlert << 2);
|
||||
|
||||
completionHandler (options);
|
||||
}
|
||||
|
||||
void didReceiveNotificationResponseWithCompletionHandler (UNNotificationResponse* response,
|
||||
void (^completionHandler)())
|
||||
{
|
||||
const bool remote = [response.notification.request.trigger isKindOfClass: [UNPushNotificationTrigger class]];
|
||||
|
||||
auto actionString = nsStringToJuce (response.actionIdentifier);
|
||||
|
||||
if (actionString == "com.apple.UNNotificationDefaultActionIdentifier")
|
||||
actionString.clear();
|
||||
else if (actionString == "com.apple.UNNotificationDismissActionIdentifier")
|
||||
actionString = "com.juce.NotificationDeleted";
|
||||
|
||||
auto n = PushNotificationsDelegateDetails::unNotificationToJuceNotification (response.notification);
|
||||
|
||||
String responseString;
|
||||
|
||||
if ([response isKindOfClass: [UNTextInputNotificationResponse class]])
|
||||
{
|
||||
UNTextInputNotificationResponse* textResponse = (UNTextInputNotificationResponse*)response;
|
||||
responseString = nsStringToJuce (textResponse.userText);
|
||||
}
|
||||
|
||||
owner.listeners.call ([&] (Listener& l) { l.handleNotificationAction (! remote, n, actionString, responseString); });
|
||||
completionHandler();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
struct Class final : public ObjCClass<NSObject<UIApplicationDelegate, UNUserNotificationCenterDelegate>>
|
||||
{
|
||||
Class()
|
||||
: ObjCClass ("JucePushNotificationsDelegate_")
|
||||
{
|
||||
addIvar<Impl*> ("self");
|
||||
|
||||
addMethod (@selector (application:didRegisterForRemoteNotificationsWithDeviceToken:), [] (id self, SEL, UIApplication*, NSData* data)
|
||||
{
|
||||
getThis (self).registeredForRemoteNotifications (data);
|
||||
});
|
||||
|
||||
addMethod (@selector (application:didFailToRegisterForRemoteNotificationsWithError:), [] (id self, SEL, UIApplication*, NSError* error)
|
||||
{
|
||||
getThis (self).failedToRegisterForRemoteNotifications (error);
|
||||
});
|
||||
|
||||
addMethod (@selector (application:didReceiveRemoteNotification:), [] (id self, SEL, UIApplication*, NSDictionary* userInfo)
|
||||
{
|
||||
getThis (self).didReceiveRemoteNotification (userInfo);
|
||||
});
|
||||
|
||||
addMethod (@selector (application:didReceiveRemoteNotification:fetchCompletionHandler:), [] (id self, SEL, UIApplication*, NSDictionary* userInfo, void (^completionHandler)(UIBackgroundFetchResult result))
|
||||
{
|
||||
getThis (self).didReceiveRemoteNotificationFetchCompletionHandler (userInfo, completionHandler);
|
||||
});
|
||||
|
||||
addMethod (@selector (application:handleActionWithIdentifier:forRemoteNotification:withResponseInfo:completionHandler:), [] (id self, SEL, UIApplication*, NSString* actionIdentifier, NSDictionary* userInfo, NSDictionary* responseInfo, void (^completionHandler)())
|
||||
{
|
||||
getThis (self).handleActionForRemoteNotificationCompletionHandler (actionIdentifier, userInfo, responseInfo, completionHandler);
|
||||
});
|
||||
|
||||
addMethod (@selector (userNotificationCenter:willPresentNotification:withCompletionHandler:), [] (id self, SEL, UNUserNotificationCenter*, UNNotification* notification, void (^completionHandler)(UNNotificationPresentationOptions options))
|
||||
{
|
||||
getThis (self).willPresentNotificationWithCompletionHandler (notification, completionHandler);
|
||||
});
|
||||
|
||||
addMethod (@selector (userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:), [] (id self, SEL, UNUserNotificationCenter*, UNNotificationResponse* response, void (^completionHandler)())
|
||||
{
|
||||
getThis (self).didReceiveNotificationResponseWithCompletionHandler (response, completionHandler);
|
||||
});
|
||||
|
||||
registerClass();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static Impl& getThis (id self) { return *getIvar<Impl*> (self, "self"); }
|
||||
static void setThis (id self, Impl* d) { object_setInstanceVariable (self, "self", d); }
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
static Class& getClass()
|
||||
{
|
||||
static Class c;
|
||||
return c;
|
||||
}
|
||||
|
||||
NSUniquePtr<NSObject<UIApplicationDelegate, UNUserNotificationCenterDelegate>> delegate { [getClass().createInstance() init] };
|
||||
|
||||
PushNotifications& owner;
|
||||
|
||||
bool initialised = false;
|
||||
String deviceToken;
|
||||
|
||||
PushNotifications::Settings settings;
|
||||
};
|
||||
|
||||
} // namespace juce
|
||||
@@ -0,0 +1,541 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
#define JUCE_PUSH_NOTIFICATIONS_IMPL 1
|
||||
|
||||
JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS
|
||||
|
||||
namespace PushNotificationsDelegateDetailsOsx
|
||||
{
|
||||
using Action = PushNotifications::Notification::Action;
|
||||
|
||||
//==============================================================================
|
||||
static NSUserNotification* juceNotificationToNSUserNotification (const PushNotifications::Notification& n)
|
||||
{
|
||||
auto notification = [[NSUserNotification alloc] init];
|
||||
|
||||
notification.title = juceStringToNS (n.title);
|
||||
notification.subtitle = juceStringToNS (n.subtitle);
|
||||
notification.informativeText = juceStringToNS (n.body);
|
||||
notification.userInfo = varToNSDictionary (n.properties);
|
||||
|
||||
auto triggerTime = Time::getCurrentTime() + RelativeTime (n.triggerIntervalSec);
|
||||
notification.deliveryDate = [NSDate dateWithTimeIntervalSince1970: (double) triggerTime.toMilliseconds() / 1000.0];
|
||||
|
||||
if (n.repeat && n.triggerIntervalSec >= 60)
|
||||
{
|
||||
auto dateComponents = [[NSDateComponents alloc] init];
|
||||
auto intervalSec = NSInteger (n.triggerIntervalSec);
|
||||
dateComponents.second = intervalSec;
|
||||
dateComponents.nanosecond = NSInteger ((n.triggerIntervalSec - (double) intervalSec) * 1000000000);
|
||||
|
||||
notification.deliveryRepeatInterval = dateComponents;
|
||||
|
||||
[dateComponents autorelease];
|
||||
}
|
||||
|
||||
auto soundToPlayString = n.soundToPlay.toString (true);
|
||||
|
||||
if (soundToPlayString == "default_os_sound")
|
||||
{
|
||||
notification.soundName = NSUserNotificationDefaultSoundName;
|
||||
}
|
||||
else if (soundToPlayString.isNotEmpty())
|
||||
{
|
||||
auto* soundName = juceStringToNS (soundToPlayString.fromLastOccurrenceOf ("/", false, false)
|
||||
.upToLastOccurrenceOf (".", false, false));
|
||||
|
||||
notification.soundName = soundName;
|
||||
}
|
||||
|
||||
notification.hasActionButton = n.actions.size() > 0;
|
||||
|
||||
if (n.actions.size() > 0)
|
||||
notification.actionButtonTitle = juceStringToNS (n.actions.getReference (0).title);
|
||||
|
||||
notification.identifier = juceStringToNS (n.identifier);
|
||||
|
||||
if (n.actions.size() > 0)
|
||||
{
|
||||
notification.hasReplyButton = n.actions.getReference (0).style == Action::text;
|
||||
notification.responsePlaceholder = juceStringToNS (n.actions.getReference (0).textInputPlaceholder);
|
||||
}
|
||||
|
||||
auto* imageDirectory = n.icon.contains ("/")
|
||||
? juceStringToNS (n.icon.upToLastOccurrenceOf ("/", false, true))
|
||||
: [NSString string];
|
||||
|
||||
auto* imageName = juceStringToNS (n.icon.fromLastOccurrenceOf ("/", false, false)
|
||||
.upToLastOccurrenceOf (".", false, false));
|
||||
auto* imageExtension = juceStringToNS (n.icon.fromLastOccurrenceOf (".", false, false));
|
||||
|
||||
NSString* imagePath = nil;
|
||||
|
||||
if ([imageDirectory length] == NSUInteger (0))
|
||||
{
|
||||
imagePath = [[NSBundle mainBundle] pathForResource: imageName
|
||||
ofType: imageExtension];
|
||||
}
|
||||
else
|
||||
{
|
||||
imagePath = [[NSBundle mainBundle] pathForResource: imageName
|
||||
ofType: imageExtension
|
||||
inDirectory: imageDirectory];
|
||||
}
|
||||
|
||||
notification.contentImage = [[NSImage alloc] initWithContentsOfFile: imagePath];
|
||||
|
||||
if (n.actions.size() > 1)
|
||||
{
|
||||
auto additionalActions = [NSMutableArray arrayWithCapacity: (NSUInteger) n.actions.size() - 1];
|
||||
|
||||
for (int a = 1; a < n.actions.size(); ++a)
|
||||
[additionalActions addObject: [NSUserNotificationAction actionWithIdentifier: juceStringToNS (n.actions[a].identifier)
|
||||
title: juceStringToNS (n.actions[a].title)]];
|
||||
|
||||
notification.additionalActions = additionalActions;
|
||||
}
|
||||
|
||||
[notification autorelease];
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static PushNotifications::Notification nsUserNotificationToJuceNotification (NSUserNotification* n)
|
||||
{
|
||||
PushNotifications::Notification notif;
|
||||
|
||||
notif.title = nsStringToJuce (n.title);
|
||||
notif.subtitle = nsStringToJuce (n.subtitle);
|
||||
notif.body = nsStringToJuce (n.informativeText);
|
||||
|
||||
notif.repeat = n.deliveryRepeatInterval != nil;
|
||||
|
||||
if (n.deliveryRepeatInterval != nil)
|
||||
{
|
||||
notif.triggerIntervalSec = (double) n.deliveryRepeatInterval.second + ((double) n.deliveryRepeatInterval.nanosecond / 1000000000.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
NSDate* dateNow = [NSDate date];
|
||||
NSDate* deliveryDate = n.deliveryDate;
|
||||
|
||||
notif.triggerIntervalSec = [dateNow timeIntervalSinceDate: deliveryDate];
|
||||
}
|
||||
|
||||
notif.soundToPlay = URL (nsStringToJuce (n.soundName));
|
||||
notif.properties = nsDictionaryToVar (n.userInfo);
|
||||
|
||||
notif.identifier = nsStringToJuce (n.identifier);
|
||||
|
||||
if (n.contentImage != nil)
|
||||
notif.icon = nsStringToJuce ([n.contentImage name]);
|
||||
|
||||
Array<Action> actions;
|
||||
|
||||
if (n.actionButtonTitle != nil)
|
||||
{
|
||||
Action action;
|
||||
action.title = nsStringToJuce (n.actionButtonTitle);
|
||||
|
||||
if (n.hasReplyButton)
|
||||
action.style = Action::text;
|
||||
|
||||
if (n.responsePlaceholder != nil)
|
||||
action.textInputPlaceholder = nsStringToJuce (n.responsePlaceholder);
|
||||
|
||||
actions.add (action);
|
||||
}
|
||||
|
||||
if (n.additionalActions != nil)
|
||||
{
|
||||
for (NSUserNotificationAction* a in n.additionalActions)
|
||||
{
|
||||
Action action;
|
||||
action.identifier = nsStringToJuce (a.identifier);
|
||||
action.title = nsStringToJuce (a.title);
|
||||
|
||||
actions.add (action);
|
||||
}
|
||||
}
|
||||
|
||||
return notif;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static var getNotificationPropertiesFromDictionaryVar (const var& dictionaryVar)
|
||||
{
|
||||
auto* dictionaryVarObject = dictionaryVar.getDynamicObject();
|
||||
|
||||
if (dictionaryVarObject == nullptr)
|
||||
return {};
|
||||
|
||||
const auto& properties = dictionaryVarObject->getProperties();
|
||||
|
||||
DynamicObject::Ptr propsVarObject = new DynamicObject();
|
||||
|
||||
for (int i = 0; i < properties.size(); ++i)
|
||||
{
|
||||
auto propertyName = properties.getName (i).toString();
|
||||
|
||||
if (propertyName == "aps")
|
||||
continue;
|
||||
|
||||
propsVarObject->setProperty (propertyName, properties.getValueAt (i));
|
||||
}
|
||||
|
||||
return var (propsVarObject.get());
|
||||
}
|
||||
|
||||
static PushNotifications::Notification nsDictionaryToJuceNotification (NSDictionary* dictionary)
|
||||
{
|
||||
const var dictionaryVar = nsDictionaryToVar (dictionary);
|
||||
|
||||
const var apsVar = dictionaryVar.getProperty ("aps", {});
|
||||
|
||||
if (! apsVar.isObject())
|
||||
return {};
|
||||
|
||||
var alertVar = apsVar.getProperty ("alert", {});
|
||||
|
||||
const var titleVar = alertVar.getProperty ("title", {});
|
||||
const var bodyVar = alertVar.isObject() ? alertVar.getProperty ("body", {}) : alertVar;
|
||||
|
||||
const var categoryVar = apsVar.getProperty ("category", {});
|
||||
const var soundVar = apsVar.getProperty ("sound", {});
|
||||
const var badgeVar = apsVar.getProperty ("badge", {});
|
||||
const var threadIdVar = apsVar.getProperty ("thread-id", {});
|
||||
|
||||
PushNotifications::Notification notification;
|
||||
|
||||
notification.title = titleVar .toString();
|
||||
notification.body = bodyVar .toString();
|
||||
notification.groupId = threadIdVar.toString();
|
||||
notification.category = categoryVar.toString();
|
||||
notification.soundToPlay = URL (soundVar.toString());
|
||||
notification.badgeNumber = (int) badgeVar;
|
||||
notification.properties = getNotificationPropertiesFromDictionaryVar (dictionaryVar);
|
||||
|
||||
return notification;
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
struct PushNotificationsDelegate
|
||||
{
|
||||
PushNotificationsDelegate() : delegate ([getClass().createInstance() init])
|
||||
{
|
||||
Class::setThis (delegate.get(), this);
|
||||
|
||||
id<NSApplicationDelegate> appDelegate = [[NSApplication sharedApplication] delegate];
|
||||
|
||||
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
|
||||
if ([appDelegate respondsToSelector: @selector (setPushNotificationsDelegate:)])
|
||||
[appDelegate performSelector: @selector (setPushNotificationsDelegate:) withObject: delegate.get()];
|
||||
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||||
|
||||
[NSUserNotificationCenter defaultUserNotificationCenter].delegate = delegate.get();
|
||||
}
|
||||
|
||||
virtual ~PushNotificationsDelegate()
|
||||
{
|
||||
[NSUserNotificationCenter defaultUserNotificationCenter].delegate = nil;
|
||||
}
|
||||
|
||||
virtual void registeredForRemoteNotifications (NSData* deviceToken) = 0;
|
||||
|
||||
virtual void failedToRegisterForRemoteNotifications (NSError* error) = 0;
|
||||
|
||||
virtual void didReceiveRemoteNotification (NSDictionary* userInfo) = 0;
|
||||
|
||||
virtual void didDeliverNotification (NSUserNotification* notification) = 0;
|
||||
|
||||
virtual void didActivateNotification (NSUserNotification* notification) = 0;
|
||||
|
||||
virtual bool shouldPresentNotification (NSUserNotification* notification) = 0;
|
||||
|
||||
protected:
|
||||
NSUniquePtr<NSObject<NSApplicationDelegate, NSUserNotificationCenterDelegate>> delegate;
|
||||
|
||||
private:
|
||||
struct Class final : public ObjCClass<NSObject<NSApplicationDelegate, NSUserNotificationCenterDelegate>>
|
||||
{
|
||||
Class() : ObjCClass<NSObject<NSApplicationDelegate, NSUserNotificationCenterDelegate>> ("JucePushNotificationsDelegate_")
|
||||
{
|
||||
addIvar<PushNotificationsDelegate*> ("self");
|
||||
|
||||
addMethod (@selector (application:didRegisterForRemoteNotificationsWithDeviceToken:), registeredForRemoteNotifications);
|
||||
addMethod (@selector (application:didFailToRegisterForRemoteNotificationsWithError:), failedToRegisterForRemoteNotifications);
|
||||
addMethod (@selector (application:didReceiveRemoteNotification:), didReceiveRemoteNotification);
|
||||
addMethod (@selector (userNotificationCenter:didDeliverNotification:), didDeliverNotification);
|
||||
addMethod (@selector (userNotificationCenter:didActivateNotification:), didActivateNotification);
|
||||
addMethod (@selector (userNotificationCenter:shouldPresentNotification:), shouldPresentNotification);
|
||||
|
||||
registerClass();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static PushNotificationsDelegate& getThis (id self) { return *getIvar<PushNotificationsDelegate*> (self, "self"); }
|
||||
static void setThis (id self, PushNotificationsDelegate* d) { object_setInstanceVariable (self, "self", d); }
|
||||
|
||||
//==============================================================================
|
||||
static void registeredForRemoteNotifications (id self, SEL, NSApplication*,
|
||||
NSData* deviceToken) { getThis (self).registeredForRemoteNotifications (deviceToken); }
|
||||
|
||||
static void failedToRegisterForRemoteNotifications (id self, SEL, NSApplication*,
|
||||
NSError* error) { getThis (self).failedToRegisterForRemoteNotifications (error); }
|
||||
|
||||
static void didReceiveRemoteNotification (id self, SEL, NSApplication*,
|
||||
NSDictionary* userInfo) { getThis (self).didReceiveRemoteNotification (userInfo); }
|
||||
|
||||
static void didDeliverNotification (id self, SEL, NSUserNotificationCenter*,
|
||||
NSUserNotification* notification) { getThis (self).didDeliverNotification (notification); }
|
||||
|
||||
static void didActivateNotification (id self, SEL, NSUserNotificationCenter*,
|
||||
NSUserNotification* notification) { getThis (self).didActivateNotification (notification); }
|
||||
|
||||
static bool shouldPresentNotification (id self, SEL, NSUserNotificationCenter*,
|
||||
NSUserNotification* notification) { return getThis (self).shouldPresentNotification (notification); }
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
static Class& getClass()
|
||||
{
|
||||
static Class c;
|
||||
return c;
|
||||
}
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
bool PushNotifications::Notification::isValid() const noexcept { return true; }
|
||||
|
||||
//==============================================================================
|
||||
struct PushNotifications::Impl : private PushNotificationsDelegate
|
||||
{
|
||||
explicit Impl (PushNotifications& p)
|
||||
: owner (p)
|
||||
{
|
||||
}
|
||||
|
||||
void requestPermissionsWithSettings (const PushNotifications::Settings& settingsToUse)
|
||||
{
|
||||
settings = settingsToUse;
|
||||
|
||||
NSRemoteNotificationType types = NSUInteger ((bool) settings.allowBadge);
|
||||
|
||||
types |= (NSUInteger) ((settings.allowSound ? NSRemoteNotificationTypeSound : 0)
|
||||
| (settings.allowAlert ? NSRemoteNotificationTypeAlert : 0));
|
||||
|
||||
[[NSApplication sharedApplication] registerForRemoteNotificationTypes: types];
|
||||
}
|
||||
|
||||
void requestSettingsUsed()
|
||||
{
|
||||
settings.allowBadge = [NSApplication sharedApplication].enabledRemoteNotificationTypes & NSRemoteNotificationTypeBadge;
|
||||
|
||||
settings.allowSound = [NSApplication sharedApplication].enabledRemoteNotificationTypes & NSRemoteNotificationTypeSound;
|
||||
settings.allowAlert = [NSApplication sharedApplication].enabledRemoteNotificationTypes & NSRemoteNotificationTypeAlert;
|
||||
|
||||
owner.listeners.call ([&] (Listener& l) { l.notificationSettingsReceived (settings); });
|
||||
}
|
||||
|
||||
bool areNotificationsEnabled() const { return true; }
|
||||
|
||||
void sendLocalNotification (const Notification& n)
|
||||
{
|
||||
auto* notification = PushNotificationsDelegateDetailsOsx::juceNotificationToNSUserNotification (n);
|
||||
|
||||
[[NSUserNotificationCenter defaultUserNotificationCenter] scheduleNotification: notification];
|
||||
}
|
||||
|
||||
void getDeliveredNotifications() const
|
||||
{
|
||||
Array<PushNotifications::Notification> notifs;
|
||||
|
||||
for (NSUserNotification* n in [NSUserNotificationCenter defaultUserNotificationCenter].deliveredNotifications)
|
||||
notifs.add (PushNotificationsDelegateDetailsOsx::nsUserNotificationToJuceNotification (n));
|
||||
|
||||
owner.listeners.call ([&] (Listener& l) { l.deliveredNotificationsListReceived (notifs); });
|
||||
}
|
||||
|
||||
void removeAllDeliveredNotifications()
|
||||
{
|
||||
[[NSUserNotificationCenter defaultUserNotificationCenter] removeAllDeliveredNotifications];
|
||||
}
|
||||
|
||||
void removeDeliveredNotification (const String& identifier)
|
||||
{
|
||||
PushNotifications::Notification n;
|
||||
n.identifier = identifier;
|
||||
|
||||
auto nsNotification = PushNotificationsDelegateDetailsOsx::juceNotificationToNSUserNotification (n);
|
||||
|
||||
[[NSUserNotificationCenter defaultUserNotificationCenter] removeDeliveredNotification: nsNotification];
|
||||
}
|
||||
|
||||
void setupChannels ([[maybe_unused]] const Array<ChannelGroup>& groups, [[maybe_unused]] const Array<Channel>& channels)
|
||||
{
|
||||
}
|
||||
|
||||
void getPendingLocalNotifications() const
|
||||
{
|
||||
Array<PushNotifications::Notification> notifs;
|
||||
|
||||
for (NSUserNotification* n in [NSUserNotificationCenter defaultUserNotificationCenter].scheduledNotifications)
|
||||
notifs.add (PushNotificationsDelegateDetailsOsx::nsUserNotificationToJuceNotification (n));
|
||||
|
||||
owner.listeners.call ([&] (Listener& l) { l.pendingLocalNotificationsListReceived (notifs); });
|
||||
}
|
||||
|
||||
void removePendingLocalNotification (const String& identifier)
|
||||
{
|
||||
PushNotifications::Notification n;
|
||||
n.identifier = identifier;
|
||||
|
||||
auto nsNotification = PushNotificationsDelegateDetailsOsx::juceNotificationToNSUserNotification (n);
|
||||
|
||||
[[NSUserNotificationCenter defaultUserNotificationCenter] removeScheduledNotification: nsNotification];
|
||||
}
|
||||
|
||||
void removeAllPendingLocalNotifications()
|
||||
{
|
||||
for (NSUserNotification* n in [NSUserNotificationCenter defaultUserNotificationCenter].scheduledNotifications)
|
||||
[[NSUserNotificationCenter defaultUserNotificationCenter] removeScheduledNotification: n];
|
||||
}
|
||||
|
||||
String getDeviceToken()
|
||||
{
|
||||
// You need to call requestPermissionsWithSettings() first.
|
||||
jassert (initialised);
|
||||
|
||||
return deviceToken;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
//PushNotificationsDelegate
|
||||
void registeredForRemoteNotifications (NSData* deviceTokenToUse) override
|
||||
{
|
||||
deviceToken = [deviceTokenToUse]() -> String
|
||||
{
|
||||
auto length = deviceTokenToUse.length;
|
||||
|
||||
if (auto* buffer = (const unsigned char*) deviceTokenToUse.bytes)
|
||||
{
|
||||
NSMutableString* hexString = [NSMutableString stringWithCapacity: (length * 2)];
|
||||
|
||||
for (NSUInteger i = 0; i < length; ++i)
|
||||
[hexString appendFormat:@"%02x", buffer[i]];
|
||||
|
||||
return nsStringToJuce ([hexString copy]);
|
||||
}
|
||||
|
||||
return {};
|
||||
}();
|
||||
|
||||
initialised = true;
|
||||
|
||||
owner.listeners.call ([&] (Listener& l) { l.deviceTokenRefreshed (deviceToken); });
|
||||
}
|
||||
|
||||
void failedToRegisterForRemoteNotifications ([[maybe_unused]] NSError* error) override
|
||||
{
|
||||
deviceToken.clear();
|
||||
}
|
||||
|
||||
void didReceiveRemoteNotification (NSDictionary* userInfo) override
|
||||
{
|
||||
auto n = PushNotificationsDelegateDetailsOsx::nsDictionaryToJuceNotification (userInfo);
|
||||
owner.listeners.call ([&] (Listener& l) { l.handleNotification (true, n); });
|
||||
}
|
||||
|
||||
void didDeliverNotification ([[maybe_unused]] NSUserNotification* notification) override
|
||||
{
|
||||
}
|
||||
|
||||
void didActivateNotification (NSUserNotification* notification) override
|
||||
{
|
||||
auto n = PushNotificationsDelegateDetailsOsx::nsUserNotificationToJuceNotification (notification);
|
||||
|
||||
if (notification.activationType == NSUserNotificationActivationTypeContentsClicked)
|
||||
{
|
||||
owner.listeners.call ([&] (Listener& l) { l.handleNotification (notification.remote, n); });
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto actionIdentifier = nsStringToJuce ([&]
|
||||
{
|
||||
if (notification.additionalActivationAction != nil)
|
||||
return notification.additionalActivationAction.identifier;
|
||||
|
||||
return notification.actionButtonTitle;
|
||||
}());
|
||||
|
||||
auto reply = notification.activationType == NSUserNotificationActivationTypeReplied
|
||||
? nsStringToJuce ([notification.response string])
|
||||
: String();
|
||||
|
||||
owner.listeners.call ([&] (Listener& l) { l.handleNotificationAction (notification.remote, n, actionIdentifier, reply); });
|
||||
}
|
||||
}
|
||||
|
||||
bool shouldPresentNotification (NSUserNotification*) override { return true; }
|
||||
|
||||
void subscribeToTopic ([[maybe_unused]] const String& topic) {}
|
||||
void unsubscribeFromTopic ([[maybe_unused]] const String& topic) {}
|
||||
|
||||
void sendUpstreamMessage ([[maybe_unused]] const String& serverSenderId,
|
||||
[[maybe_unused]] const String& collapseKey,
|
||||
[[maybe_unused]] const String& messageId,
|
||||
[[maybe_unused]] const String& messageType,
|
||||
[[maybe_unused]] int timeToLive,
|
||||
[[maybe_unused]] const StringPairArray& additionalData)
|
||||
{
|
||||
}
|
||||
|
||||
private:
|
||||
PushNotifications& owner;
|
||||
|
||||
bool initialised = false;
|
||||
String deviceToken;
|
||||
|
||||
PushNotifications::Settings settings;
|
||||
};
|
||||
|
||||
JUCE_END_IGNORE_DEPRECATION_WARNINGS
|
||||
|
||||
} // namespace juce
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
class SystemTrayIconComponent::Pimpl
|
||||
{
|
||||
public:
|
||||
Pimpl (const Image& im, Window windowH) : image (im)
|
||||
{
|
||||
XWindowSystemUtilities::ScopedXLock xLock;
|
||||
|
||||
auto* display = XWindowSystem::getInstance()->getDisplay();
|
||||
|
||||
auto* screen = X11Symbols::getInstance()->xDefaultScreenOfDisplay (display);
|
||||
auto screenNumber = X11Symbols::getInstance()->xScreenNumberOfScreen (screen);
|
||||
|
||||
String screenAtom ("_NET_SYSTEM_TRAY_S");
|
||||
screenAtom << screenNumber;
|
||||
Atom selectionAtom = XWindowSystemUtilities::Atoms::getCreating (display, screenAtom.toUTF8());
|
||||
|
||||
X11Symbols::getInstance()->xGrabServer (display);
|
||||
auto managerWin = X11Symbols::getInstance()->xGetSelectionOwner (display, selectionAtom);
|
||||
|
||||
if (managerWin != None)
|
||||
X11Symbols::getInstance()->xSelectInput (display, managerWin, StructureNotifyMask);
|
||||
|
||||
X11Symbols::getInstance()->xUngrabServer (display);
|
||||
X11Symbols::getInstance()->xFlush (display);
|
||||
|
||||
if (managerWin != None)
|
||||
{
|
||||
XEvent ev = { 0 };
|
||||
ev.xclient.type = ClientMessage;
|
||||
ev.xclient.window = managerWin;
|
||||
ev.xclient.message_type = XWindowSystemUtilities::Atoms::getCreating (display, "_NET_SYSTEM_TRAY_OPCODE");
|
||||
ev.xclient.format = 32;
|
||||
ev.xclient.data.l[0] = CurrentTime;
|
||||
ev.xclient.data.l[1] = 0 /*SYSTEM_TRAY_REQUEST_DOCK*/;
|
||||
ev.xclient.data.l[2] = (long) windowH;
|
||||
ev.xclient.data.l[3] = 0;
|
||||
ev.xclient.data.l[4] = 0;
|
||||
|
||||
X11Symbols::getInstance()->xSendEvent (display, managerWin, False, NoEventMask, &ev);
|
||||
X11Symbols::getInstance()->xSync (display, False);
|
||||
}
|
||||
|
||||
// For older KDE's ...
|
||||
long atomData = 1;
|
||||
Atom trayAtom = XWindowSystemUtilities::Atoms::getCreating (display, "KWM_DOCKWINDOW");
|
||||
X11Symbols::getInstance()->xChangeProperty (display, windowH, trayAtom, trayAtom,
|
||||
32, PropModeReplace, (unsigned char*) &atomData, 1);
|
||||
|
||||
// For more recent KDE's...
|
||||
trayAtom = XWindowSystemUtilities::Atoms::getCreating (display, "_KDE_NET_WM_SYSTEM_TRAY_WINDOW_FOR");
|
||||
X11Symbols::getInstance()->xChangeProperty (display, windowH, trayAtom, XA_WINDOW,
|
||||
32, PropModeReplace, (unsigned char*) &windowH, 1);
|
||||
|
||||
// A minimum size must be specified for GNOME and Xfce, otherwise the icon is displayed with a width of 1
|
||||
if (auto* hints = X11Symbols::getInstance()->xAllocSizeHints())
|
||||
{
|
||||
hints->flags = PMinSize;
|
||||
hints->min_width = 22;
|
||||
hints->min_height = 22;
|
||||
X11Symbols::getInstance()->xSetWMNormalHints (display, windowH, hints);
|
||||
X11Symbols::getInstance()->xFree (hints);
|
||||
}
|
||||
}
|
||||
|
||||
Image image;
|
||||
|
||||
private:
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
|
||||
};
|
||||
|
||||
|
||||
//==============================================================================
|
||||
void SystemTrayIconComponent::setIconImage (const Image& colourImage, const Image&)
|
||||
{
|
||||
pimpl.reset();
|
||||
|
||||
if (colourImage.isValid())
|
||||
{
|
||||
if (! isOnDesktop())
|
||||
addToDesktop (0);
|
||||
|
||||
pimpl.reset (new Pimpl (colourImage, (Window) getWindowHandle()));
|
||||
|
||||
setVisible (true);
|
||||
toFront (false);
|
||||
}
|
||||
|
||||
repaint();
|
||||
}
|
||||
|
||||
void SystemTrayIconComponent::paint (Graphics& g)
|
||||
{
|
||||
if (pimpl != nullptr)
|
||||
g.drawImage (pimpl->image, getLocalBounds().toFloat(),
|
||||
RectanglePlacement::xLeft | RectanglePlacement::yTop | RectanglePlacement::onlyReduceInSize);
|
||||
}
|
||||
|
||||
void SystemTrayIconComponent::setIconTooltip (const String& /*tooltip*/)
|
||||
{
|
||||
// xxx Not implemented!
|
||||
}
|
||||
|
||||
void SystemTrayIconComponent::setHighlighted (bool)
|
||||
{
|
||||
// xxx Not implemented!
|
||||
}
|
||||
|
||||
void SystemTrayIconComponent::showInfoBubble (const String& /*title*/, const String& /*content*/)
|
||||
{
|
||||
// xxx Not implemented!
|
||||
}
|
||||
|
||||
void SystemTrayIconComponent::hideInfoBubble()
|
||||
{
|
||||
// xxx Not implemented!
|
||||
}
|
||||
|
||||
void* SystemTrayIconComponent::getNativeHandle() const
|
||||
{
|
||||
return getWindowHandle();
|
||||
}
|
||||
|
||||
} // namespace juce
|
||||
@@ -0,0 +1,451 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
JUCE_BEGIN_IGNORE_DEPRECATION_WARNINGS
|
||||
|
||||
extern NSMenu* createNSMenu (const PopupMenu&, const String& name, int topLevelMenuId,
|
||||
int topLevelIndex, bool addDelegate);
|
||||
|
||||
//==============================================================================
|
||||
struct StatusItemContainer : public Timer
|
||||
{
|
||||
//==============================================================================
|
||||
StatusItemContainer (SystemTrayIconComponent& iconComp, const Image& im)
|
||||
: owner (iconComp), statusIcon (imageToNSImage (ScaledImage (im)))
|
||||
{
|
||||
}
|
||||
|
||||
virtual void configureIcon() = 0;
|
||||
virtual void setHighlighted (bool shouldHighlight) = 0;
|
||||
|
||||
//==============================================================================
|
||||
void setIconSize()
|
||||
{
|
||||
[statusIcon.get() setSize: NSMakeSize (20.0f, 20.0f)];
|
||||
}
|
||||
|
||||
void updateIcon (const Image& newImage)
|
||||
{
|
||||
statusIcon.reset (imageToNSImage (ScaledImage (newImage)));
|
||||
setIconSize();
|
||||
configureIcon();
|
||||
}
|
||||
|
||||
void showMenu (const PopupMenu& menu)
|
||||
{
|
||||
if (NSMenu* m = createNSMenu (menu, "MenuBarItem", -2, -3, true))
|
||||
{
|
||||
setHighlighted (true);
|
||||
stopTimer();
|
||||
|
||||
// There's currently no good alternative to this.
|
||||
[statusItem.get() popUpStatusItemMenu: m];
|
||||
|
||||
startTimer (1);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void timerCallback() override
|
||||
{
|
||||
stopTimer();
|
||||
setHighlighted (false);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
SystemTrayIconComponent& owner;
|
||||
|
||||
NSUniquePtr<NSStatusItem> statusItem;
|
||||
NSUniquePtr<NSImage> statusIcon;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (StatusItemContainer)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
struct ButtonBasedStatusItem final : public StatusItemContainer
|
||||
{
|
||||
//==============================================================================
|
||||
ButtonBasedStatusItem (SystemTrayIconComponent& iconComp, const Image& im)
|
||||
: StatusItemContainer (iconComp, im)
|
||||
{
|
||||
static ButtonEventForwarderClass cls;
|
||||
eventForwarder.reset ([cls.createInstance() init]);
|
||||
ButtonEventForwarderClass::setOwner (eventForwarder.get(), this);
|
||||
|
||||
setIconSize();
|
||||
configureIcon();
|
||||
|
||||
statusItem.reset ([[[NSStatusBar systemStatusBar] statusItemWithLength: NSSquareStatusItemLength] retain]);
|
||||
auto button = [statusItem.get() button];
|
||||
button.image = statusIcon.get();
|
||||
button.target = eventForwarder.get();
|
||||
button.action = @selector (handleEvent:);
|
||||
[button sendActionOn: NSEventMaskLeftMouseDown | NSEventMaskRightMouseDown | NSEventMaskScrollWheel];
|
||||
}
|
||||
|
||||
~ButtonBasedStatusItem() override
|
||||
{
|
||||
[statusItem.get() button].image = nullptr;
|
||||
}
|
||||
|
||||
void configureIcon() override
|
||||
{
|
||||
[statusIcon.get() setTemplate: true];
|
||||
[statusItem.get() button].image = statusIcon.get();
|
||||
}
|
||||
|
||||
void setHighlighted (bool shouldHighlight) override
|
||||
{
|
||||
[[statusItem.get() button] setHighlighted: shouldHighlight];
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void handleEvent()
|
||||
{
|
||||
auto e = [NSApp currentEvent];
|
||||
NSEventType type = [e type];
|
||||
|
||||
const bool isLeft = (type == NSEventTypeLeftMouseDown);
|
||||
const bool isRight = (type == NSEventTypeRightMouseDown);
|
||||
|
||||
if (owner.isCurrentlyBlockedByAnotherModalComponent())
|
||||
{
|
||||
if (isLeft || isRight)
|
||||
if (auto* current = Component::getCurrentlyModalComponent())
|
||||
current->inputAttemptWhenModal();
|
||||
}
|
||||
else
|
||||
{
|
||||
auto eventMods = ComponentPeer::getCurrentModifiersRealtime();
|
||||
|
||||
if (([e modifierFlags] & NSEventModifierFlagCommand) != 0)
|
||||
eventMods = eventMods.withFlags (ModifierKeys::commandModifier);
|
||||
|
||||
auto now = Time::getCurrentTime();
|
||||
auto mouseSource = Desktop::getInstance().getMainMouseSource();
|
||||
auto pressure = (float) e.pressure;
|
||||
|
||||
if (isLeft || isRight)
|
||||
{
|
||||
owner.mouseDown ({ mouseSource, {},
|
||||
eventMods.withFlags (isLeft ? ModifierKeys::leftButtonModifier
|
||||
: ModifierKeys::rightButtonModifier),
|
||||
pressure,
|
||||
MouseInputSource::defaultOrientation, MouseInputSource::defaultRotation,
|
||||
MouseInputSource::defaultTiltX, MouseInputSource::defaultTiltY,
|
||||
&owner, &owner, now, {}, now, 1, false });
|
||||
|
||||
owner.mouseUp ({ mouseSource, {},
|
||||
eventMods.withoutMouseButtons(),
|
||||
pressure,
|
||||
MouseInputSource::defaultOrientation, MouseInputSource::defaultRotation,
|
||||
MouseInputSource::defaultTiltX, MouseInputSource::defaultTiltY,
|
||||
&owner, &owner, now, {}, now, 1, false });
|
||||
}
|
||||
else if (type == NSEventTypeMouseMoved)
|
||||
{
|
||||
owner.mouseMove (MouseEvent (mouseSource, {}, eventMods, pressure,
|
||||
MouseInputSource::defaultOrientation, MouseInputSource::defaultRotation,
|
||||
MouseInputSource::defaultTiltX, MouseInputSource::defaultTiltY,
|
||||
&owner, &owner, now, {}, now, 1, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
class ButtonEventForwarderClass final : public ObjCClass<NSObject>
|
||||
{
|
||||
public:
|
||||
ButtonEventForwarderClass() : ObjCClass<NSObject> ("JUCEButtonEventForwarderClass_")
|
||||
{
|
||||
addIvar<ButtonBasedStatusItem*> ("owner");
|
||||
|
||||
addMethod (@selector (handleEvent:), handleEvent);
|
||||
|
||||
registerClass();
|
||||
}
|
||||
|
||||
static ButtonBasedStatusItem* getOwner (id self) { return getIvar<ButtonBasedStatusItem*> (self, "owner"); }
|
||||
static void setOwner (id self, ButtonBasedStatusItem* owner) { object_setInstanceVariable (self, "owner", owner); }
|
||||
|
||||
private:
|
||||
static void handleEvent (id self, SEL, id)
|
||||
{
|
||||
if (auto* owner = getOwner (self))
|
||||
owner->handleEvent();
|
||||
}
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
NSUniquePtr<NSObject> eventForwarder;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
struct ViewBasedStatusItem final : public StatusItemContainer
|
||||
{
|
||||
//==============================================================================
|
||||
ViewBasedStatusItem (SystemTrayIconComponent& iconComp, const Image& im)
|
||||
: StatusItemContainer (iconComp, im)
|
||||
{
|
||||
static SystemTrayViewClass cls;
|
||||
view.reset ([cls.createInstance() init]);
|
||||
SystemTrayViewClass::setOwner (view.get(), this);
|
||||
SystemTrayViewClass::setImage (view.get(), statusIcon.get());
|
||||
|
||||
setIconSize();
|
||||
|
||||
statusItem.reset ([[[NSStatusBar systemStatusBar] statusItemWithLength: NSSquareStatusItemLength] retain]);
|
||||
[statusItem.get() setView: view.get()];
|
||||
|
||||
SystemTrayViewClass::frameChanged (view.get(), SEL(), nullptr);
|
||||
|
||||
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
|
||||
[[NSNotificationCenter defaultCenter] addObserver: view.get()
|
||||
selector: @selector (frameChanged:)
|
||||
name: NSWindowDidMoveNotification
|
||||
object: nil];
|
||||
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||||
}
|
||||
|
||||
~ViewBasedStatusItem() override
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver: view.get()];
|
||||
[[NSStatusBar systemStatusBar] removeStatusItem: statusItem.get()];
|
||||
SystemTrayViewClass::setOwner (view.get(), nullptr);
|
||||
SystemTrayViewClass::setImage (view.get(), nil);
|
||||
}
|
||||
|
||||
void configureIcon() override
|
||||
{
|
||||
SystemTrayViewClass::setImage (view.get(), statusIcon.get());
|
||||
[statusItem.get() setView: view.get()];
|
||||
}
|
||||
|
||||
void setHighlighted (bool shouldHighlight) override
|
||||
{
|
||||
isHighlighted = shouldHighlight;
|
||||
[view.get() setNeedsDisplay: true];
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void handleStatusItemAction (NSEvent* e)
|
||||
{
|
||||
NSEventType type = [e type];
|
||||
|
||||
const bool isLeft = (type == NSEventTypeLeftMouseDown || type == NSEventTypeLeftMouseUp);
|
||||
const bool isRight = (type == NSEventTypeRightMouseDown || type == NSEventTypeRightMouseUp);
|
||||
|
||||
if (owner.isCurrentlyBlockedByAnotherModalComponent())
|
||||
{
|
||||
if (isLeft || isRight)
|
||||
if (auto* current = Component::getCurrentlyModalComponent())
|
||||
current->inputAttemptWhenModal();
|
||||
}
|
||||
else
|
||||
{
|
||||
auto eventMods = ComponentPeer::getCurrentModifiersRealtime();
|
||||
|
||||
if (([e modifierFlags] & NSEventModifierFlagCommand) != 0)
|
||||
eventMods = eventMods.withFlags (ModifierKeys::commandModifier);
|
||||
|
||||
auto now = Time::getCurrentTime();
|
||||
auto mouseSource = Desktop::getInstance().getMainMouseSource();
|
||||
auto pressure = (float) e.pressure;
|
||||
|
||||
if (isLeft || isRight) // Only mouse up is sent by the OS, so simulate a down/up
|
||||
{
|
||||
setHighlighted (true);
|
||||
startTimer (150);
|
||||
|
||||
owner.mouseDown (MouseEvent (mouseSource, {},
|
||||
eventMods.withFlags (isLeft ? ModifierKeys::leftButtonModifier
|
||||
: ModifierKeys::rightButtonModifier),
|
||||
pressure, MouseInputSource::defaultOrientation, MouseInputSource::defaultRotation,
|
||||
MouseInputSource::defaultTiltX, MouseInputSource::defaultTiltY,
|
||||
&owner, &owner, now, {}, now, 1, false));
|
||||
|
||||
owner.mouseUp (MouseEvent (mouseSource, {}, eventMods.withoutMouseButtons(), pressure,
|
||||
MouseInputSource::defaultOrientation, MouseInputSource::defaultRotation,
|
||||
MouseInputSource::defaultTiltX, MouseInputSource::defaultTiltY,
|
||||
&owner, &owner, now, {}, now, 1, false));
|
||||
}
|
||||
else if (type == NSEventTypeMouseMoved)
|
||||
{
|
||||
owner.mouseMove (MouseEvent (mouseSource, {}, eventMods, pressure,
|
||||
MouseInputSource::defaultOrientation, MouseInputSource::defaultRotation,
|
||||
MouseInputSource::defaultTiltX, MouseInputSource::defaultTiltY,
|
||||
&owner, &owner, now, {}, now, 1, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
struct SystemTrayViewClass final : public ObjCClass<NSControl>
|
||||
{
|
||||
SystemTrayViewClass() : ObjCClass<NSControl> ("JUCESystemTrayView_")
|
||||
{
|
||||
addIvar<ViewBasedStatusItem*> ("owner");
|
||||
addIvar<NSImage*> ("image");
|
||||
|
||||
addMethod (@selector (mouseDown:), handleEventDown);
|
||||
addMethod (@selector (rightMouseDown:), handleEventDown);
|
||||
addMethod (@selector (drawRect:), drawRect);
|
||||
|
||||
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
|
||||
addMethod (@selector (frameChanged:), frameChanged);
|
||||
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||||
|
||||
registerClass();
|
||||
}
|
||||
|
||||
static ViewBasedStatusItem* getOwner (id self) { return getIvar<ViewBasedStatusItem*> (self, "owner"); }
|
||||
static NSImage* getImage (id self) { return getIvar<NSImage*> (self, "image"); }
|
||||
static void setOwner (id self, ViewBasedStatusItem* owner) { object_setInstanceVariable (self, "owner", owner); }
|
||||
static void setImage (id self, NSImage* image) { object_setInstanceVariable (self, "image", image); }
|
||||
|
||||
static void frameChanged (id self, SEL, NSNotification*)
|
||||
{
|
||||
if (auto* owner = getOwner (self))
|
||||
{
|
||||
NSRect r = [[[owner->statusItem.get() view] window] frame];
|
||||
NSRect sr = [[[NSScreen screens] objectAtIndex: 0] frame];
|
||||
r.origin.y = sr.size.height - r.origin.y - r.size.height;
|
||||
owner->owner.setBounds (convertToRectInt (r));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static void handleEventDown (id self, SEL, NSEvent* e)
|
||||
{
|
||||
if (auto* owner = getOwner (self))
|
||||
owner->handleStatusItemAction (e);
|
||||
}
|
||||
|
||||
static void drawRect (id self, SEL, NSRect)
|
||||
{
|
||||
NSRect bounds = [self bounds];
|
||||
|
||||
if (auto* owner = getOwner (self))
|
||||
[owner->statusItem.get() drawStatusBarBackgroundInRect: bounds
|
||||
withHighlight: owner->isHighlighted];
|
||||
|
||||
if (NSImage* const im = getImage (self))
|
||||
{
|
||||
NSSize imageSize = [im size];
|
||||
|
||||
[im drawInRect: NSMakeRect (bounds.origin.x + ((bounds.size.width - imageSize.width) / 2.0f),
|
||||
bounds.origin.y + ((bounds.size.height - imageSize.height) / 2.0f),
|
||||
imageSize.width, imageSize.height)
|
||||
fromRect: NSZeroRect
|
||||
operation: NSCompositingOperationSourceOver
|
||||
fraction: 1.0f];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
NSUniquePtr<NSControl> view;
|
||||
bool isHighlighted = false;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class SystemTrayIconComponent::Pimpl
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
Pimpl (SystemTrayIconComponent& iconComp, const Image& im)
|
||||
: statusItemHolder (std::make_unique<ButtonBasedStatusItem> (iconComp, im))
|
||||
{
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
std::unique_ptr<StatusItemContainer> statusItemHolder;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
void SystemTrayIconComponent::setIconImage (const Image&, const Image& templateImage)
|
||||
{
|
||||
if (templateImage.isValid())
|
||||
{
|
||||
if (pimpl == nullptr)
|
||||
pimpl.reset (new Pimpl (*this, templateImage));
|
||||
else
|
||||
pimpl->statusItemHolder->updateIcon (templateImage);
|
||||
}
|
||||
else
|
||||
{
|
||||
pimpl.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void SystemTrayIconComponent::setIconTooltip (const String&)
|
||||
{
|
||||
// xxx not yet implemented!
|
||||
}
|
||||
|
||||
void SystemTrayIconComponent::setHighlighted (bool shouldHighlight)
|
||||
{
|
||||
if (pimpl != nullptr)
|
||||
pimpl->statusItemHolder->setHighlighted (shouldHighlight);
|
||||
}
|
||||
|
||||
void SystemTrayIconComponent::showInfoBubble (const String& /*title*/, const String& /*content*/)
|
||||
{
|
||||
// xxx Not implemented!
|
||||
}
|
||||
|
||||
void SystemTrayIconComponent::hideInfoBubble()
|
||||
{
|
||||
// xxx Not implemented!
|
||||
}
|
||||
|
||||
void* SystemTrayIconComponent::getNativeHandle() const
|
||||
{
|
||||
return pimpl != nullptr ? pimpl->statusItemHolder->statusItem.get() : nullptr;
|
||||
}
|
||||
|
||||
void SystemTrayIconComponent::showDropdownMenu (const PopupMenu& menu)
|
||||
{
|
||||
if (pimpl != nullptr)
|
||||
pimpl->statusItemHolder->showMenu (menu);
|
||||
}
|
||||
|
||||
JUCE_END_IGNORE_DEPRECATION_WARNINGS
|
||||
|
||||
} // namespace juce
|
||||
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
extern void* getUser32Function (const char*);
|
||||
|
||||
namespace IconConverters
|
||||
{
|
||||
extern HICON createHICONFromImage (const Image&, BOOL isIcon, int hotspotX, int hotspotY);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
class SystemTrayIconComponent::Pimpl
|
||||
{
|
||||
public:
|
||||
Pimpl (SystemTrayIconComponent& owner_, HICON hicon, HWND hwnd)
|
||||
: owner (owner_),
|
||||
originalWndProc ((WNDPROC) GetWindowLongPtr (hwnd, GWLP_WNDPROC)),
|
||||
taskbarCreatedMessage (RegisterWindowMessage (TEXT ("TaskbarCreated")))
|
||||
{
|
||||
SetWindowLongPtr (hwnd, GWLP_WNDPROC, (LONG_PTR) hookedWndProc);
|
||||
|
||||
zerostruct (iconData);
|
||||
iconData.cbSize = sizeof (iconData);
|
||||
iconData.hWnd = hwnd;
|
||||
iconData.uID = (UINT) (pointer_sized_int) hwnd;
|
||||
iconData.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
|
||||
iconData.uCallbackMessage = WM_TRAYNOTIFY;
|
||||
iconData.hIcon = hicon;
|
||||
|
||||
notify (NIM_ADD);
|
||||
|
||||
// In order to receive the "TaskbarCreated" message, we need to request that it's not filtered out.
|
||||
// (Need to load dynamically, as ChangeWindowMessageFilter is only available in Vista and later)
|
||||
typedef BOOL (WINAPI* ChangeWindowMessageFilterType) (UINT, DWORD);
|
||||
|
||||
if (ChangeWindowMessageFilterType changeWindowMessageFilter
|
||||
= (ChangeWindowMessageFilterType) getUser32Function ("ChangeWindowMessageFilter"))
|
||||
changeWindowMessageFilter (taskbarCreatedMessage, 1 /* MSGFLT_ADD */);
|
||||
}
|
||||
|
||||
~Pimpl()
|
||||
{
|
||||
SetWindowLongPtr (iconData.hWnd, GWLP_WNDPROC, (LONG_PTR) originalWndProc);
|
||||
|
||||
iconData.uFlags = 0;
|
||||
notify (NIM_DELETE);
|
||||
DestroyIcon (iconData.hIcon);
|
||||
}
|
||||
|
||||
void updateIcon (HICON hicon)
|
||||
{
|
||||
HICON oldIcon = iconData.hIcon;
|
||||
|
||||
iconData.hIcon = hicon;
|
||||
iconData.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
|
||||
notify (NIM_MODIFY);
|
||||
|
||||
DestroyIcon (oldIcon);
|
||||
}
|
||||
|
||||
void setToolTip (const String& toolTip)
|
||||
{
|
||||
iconData.uFlags = NIF_TIP;
|
||||
toolTip.copyToUTF16 (iconData.szTip, sizeof (iconData.szTip) - 1);
|
||||
notify (NIM_MODIFY);
|
||||
}
|
||||
|
||||
void handleTaskBarEvent (const LPARAM lParam)
|
||||
{
|
||||
if (owner.isCurrentlyBlockedByAnotherModalComponent())
|
||||
{
|
||||
if (lParam == WM_LBUTTONDOWN || lParam == WM_RBUTTONDOWN
|
||||
|| lParam == WM_LBUTTONDBLCLK || lParam == WM_RBUTTONDBLCLK)
|
||||
{
|
||||
if (auto* current = Component::getCurrentlyModalComponent())
|
||||
current->inputAttemptWhenModal();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModifierKeys eventMods (ComponentPeer::getCurrentModifiersRealtime());
|
||||
|
||||
if (lParam == WM_LBUTTONDOWN || lParam == WM_LBUTTONDBLCLK)
|
||||
eventMods = eventMods.withFlags (ModifierKeys::leftButtonModifier);
|
||||
else if (lParam == WM_RBUTTONDOWN || lParam == WM_RBUTTONDBLCLK)
|
||||
eventMods = eventMods.withFlags (ModifierKeys::rightButtonModifier);
|
||||
else if (lParam == WM_LBUTTONUP || lParam == WM_RBUTTONUP)
|
||||
eventMods = eventMods.withoutMouseButtons();
|
||||
|
||||
const Time eventTime (getMouseEventTime());
|
||||
|
||||
const MouseEvent e (Desktop::getInstance().getMainMouseSource(), {}, eventMods,
|
||||
MouseInputSource::defaultPressure, MouseInputSource::defaultOrientation,
|
||||
MouseInputSource::defaultRotation, MouseInputSource::defaultTiltX, MouseInputSource::defaultTiltY,
|
||||
&owner, &owner, eventTime, {}, eventTime, 1, false);
|
||||
|
||||
if (lParam == WM_LBUTTONDOWN || lParam == WM_RBUTTONDOWN)
|
||||
{
|
||||
SetFocus (iconData.hWnd);
|
||||
SetForegroundWindow (iconData.hWnd);
|
||||
owner.mouseDown (e);
|
||||
}
|
||||
else if (lParam == WM_LBUTTONUP || lParam == WM_RBUTTONUP)
|
||||
{
|
||||
owner.mouseUp (e);
|
||||
}
|
||||
else if (lParam == WM_LBUTTONDBLCLK || lParam == WM_RBUTTONDBLCLK)
|
||||
{
|
||||
owner.mouseDoubleClick (e);
|
||||
}
|
||||
else if (lParam == WM_MOUSEMOVE)
|
||||
{
|
||||
owner.mouseMove (e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Pimpl* getPimpl (HWND hwnd)
|
||||
{
|
||||
if (JuceWindowIdentifier::isJUCEWindow (hwnd))
|
||||
if (ComponentPeer* peer = (ComponentPeer*) GetWindowLongPtr (hwnd, 8))
|
||||
if (SystemTrayIconComponent* const iconComp = dynamic_cast<SystemTrayIconComponent*> (&(peer->getComponent())))
|
||||
return iconComp->pimpl.get();
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static LRESULT CALLBACK hookedWndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
if (Pimpl* const p = getPimpl (hwnd))
|
||||
return p->windowProc (hwnd, message, wParam, lParam);
|
||||
|
||||
return DefWindowProcW (hwnd, message, wParam, lParam);
|
||||
}
|
||||
|
||||
LRESULT windowProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
if (message == WM_TRAYNOTIFY)
|
||||
{
|
||||
handleTaskBarEvent (lParam);
|
||||
}
|
||||
else if (message == taskbarCreatedMessage)
|
||||
{
|
||||
iconData.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
|
||||
notify (NIM_ADD);
|
||||
}
|
||||
|
||||
return CallWindowProc (originalWndProc, hwnd, message, wParam, lParam);
|
||||
}
|
||||
|
||||
void showBubble (const String& title, const String& content)
|
||||
{
|
||||
iconData.uFlags = 0x10 /*NIF_INFO*/;
|
||||
title.copyToUTF16 (iconData.szInfoTitle, sizeof (iconData.szInfoTitle) - 1);
|
||||
content.copyToUTF16 (iconData.szInfo, sizeof (iconData.szInfo) - 1);
|
||||
notify (NIM_MODIFY);
|
||||
}
|
||||
|
||||
SystemTrayIconComponent& owner;
|
||||
NOTIFYICONDATA iconData;
|
||||
|
||||
private:
|
||||
WNDPROC originalWndProc;
|
||||
const DWORD taskbarCreatedMessage;
|
||||
enum { WM_TRAYNOTIFY = WM_USER + 100 };
|
||||
|
||||
void notify (DWORD message) noexcept { Shell_NotifyIcon (message, &iconData); }
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
void SystemTrayIconComponent::setIconImage (const Image& colourImage, const Image&)
|
||||
{
|
||||
if (colourImage.isValid())
|
||||
{
|
||||
HICON hicon = IconConverters::createHICONFromImage (colourImage, TRUE, 0, 0);
|
||||
|
||||
if (pimpl == nullptr)
|
||||
pimpl.reset (new Pimpl (*this, hicon, (HWND) getWindowHandle()));
|
||||
else
|
||||
pimpl->updateIcon (hicon);
|
||||
}
|
||||
else
|
||||
{
|
||||
pimpl.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void SystemTrayIconComponent::setIconTooltip (const String& tooltip)
|
||||
{
|
||||
if (pimpl != nullptr)
|
||||
pimpl->setToolTip (tooltip);
|
||||
}
|
||||
|
||||
void SystemTrayIconComponent::setHighlighted (bool)
|
||||
{
|
||||
// N/A on Windows.
|
||||
}
|
||||
|
||||
void SystemTrayIconComponent::showInfoBubble (const String& title, const String& content)
|
||||
{
|
||||
if (pimpl != nullptr)
|
||||
pimpl->showBubble (title, content);
|
||||
}
|
||||
|
||||
void SystemTrayIconComponent::hideInfoBubble()
|
||||
{
|
||||
showInfoBubble (String(), String());
|
||||
}
|
||||
|
||||
void* SystemTrayIconComponent::getNativeHandle() const
|
||||
{
|
||||
return pimpl != nullptr ? &(pimpl->iconData) : nullptr;
|
||||
}
|
||||
|
||||
} // namespace juce
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
class UIViewComponent::Pimpl : public ComponentMovementWatcher
|
||||
{
|
||||
public:
|
||||
Pimpl (UIView* v, Component& comp)
|
||||
: ComponentMovementWatcher (&comp),
|
||||
view (v),
|
||||
owner (comp)
|
||||
{
|
||||
[view retain];
|
||||
|
||||
if (owner.isShowing())
|
||||
componentPeerChanged();
|
||||
}
|
||||
|
||||
~Pimpl() override
|
||||
{
|
||||
[view removeFromSuperview];
|
||||
[view release];
|
||||
}
|
||||
|
||||
void componentMovedOrResized (bool /*wasMoved*/, bool /*wasResized*/) override
|
||||
{
|
||||
auto* topComp = owner.getTopLevelComponent();
|
||||
|
||||
if (topComp->getPeer() != nullptr)
|
||||
{
|
||||
auto pos = topComp->getLocalPoint (&owner, Point<int>());
|
||||
|
||||
[view setFrame: CGRectMake ((float) pos.x, (float) pos.y,
|
||||
(float) owner.getWidth(), (float) owner.getHeight())];
|
||||
}
|
||||
}
|
||||
|
||||
void componentPeerChanged() override
|
||||
{
|
||||
auto* peer = owner.getPeer();
|
||||
|
||||
if (currentPeer != peer)
|
||||
{
|
||||
if ([view superview] != nil)
|
||||
[view removeFromSuperview]; // Must be careful not to call this unless it's required - e.g. some Apple AU views
|
||||
// override the call and use it as a sign that they're being deleted, which breaks everything..
|
||||
currentPeer = peer;
|
||||
|
||||
if (peer != nullptr)
|
||||
{
|
||||
UIView* peerView = (UIView*) peer->getNativeHandle();
|
||||
[peerView addSubview: view];
|
||||
componentMovedOrResized (false, false);
|
||||
}
|
||||
}
|
||||
|
||||
[view setHidden: ! owner.isShowing()];
|
||||
}
|
||||
|
||||
void componentVisibilityChanged() override
|
||||
{
|
||||
componentPeerChanged();
|
||||
}
|
||||
|
||||
Rectangle<int> getViewBounds() const
|
||||
{
|
||||
CGRect r = [view frame];
|
||||
return Rectangle<int> ((int) r.size.width, (int) r.size.height);
|
||||
}
|
||||
|
||||
UIView* const view;
|
||||
|
||||
private:
|
||||
Component& owner;
|
||||
ComponentPeer* currentPeer = nullptr;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
UIViewComponent::UIViewComponent() {}
|
||||
UIViewComponent::~UIViewComponent()
|
||||
{
|
||||
AccessibilityHandler::setNativeChildForComponent (*this, nullptr);
|
||||
}
|
||||
|
||||
void UIViewComponent::setView (void* view)
|
||||
{
|
||||
if (view != getView())
|
||||
{
|
||||
pimpl.reset();
|
||||
|
||||
if (view != nullptr)
|
||||
pimpl.reset (new Pimpl ((UIView*) view, *this));
|
||||
|
||||
AccessibilityHandler::setNativeChildForComponent (*this, getView());
|
||||
}
|
||||
}
|
||||
|
||||
void* UIViewComponent::getView() const
|
||||
{
|
||||
return pimpl == nullptr ? nullptr : pimpl->view;
|
||||
}
|
||||
|
||||
void UIViewComponent::resizeToFitView()
|
||||
{
|
||||
if (pimpl != nullptr)
|
||||
setBounds (pimpl->getViewBounds());
|
||||
}
|
||||
|
||||
void UIViewComponent::paint (Graphics&) {}
|
||||
|
||||
std::unique_ptr<AccessibilityHandler> UIViewComponent::createAccessibilityHandler()
|
||||
{
|
||||
return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::group);
|
||||
}
|
||||
|
||||
} // namespace juce
|
||||
+953
@@ -0,0 +1,953 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
// This byte-code is generated from native/java/app/com/rmsl/juce/JuceWebViewClasses.java with min sdk version 23.
|
||||
// See juce_core/native/java/README.txt on how to generate this byte-code.
|
||||
static const uint8 juceWebViewClasses[] =
|
||||
{
|
||||
0x1f, 0x8b, 0x08, 0x08, 0x96, 0xd6, 0x11, 0x65, 0x00, 0x03, 0x63, 0x6c,
|
||||
0x61, 0x73, 0x73, 0x65, 0x73, 0x2e, 0x64, 0x65, 0x78, 0x00, 0x95, 0x59,
|
||||
0x6b, 0x6c, 0x1c, 0x57, 0x15, 0x3e, 0x33, 0xb3, 0x0f, 0xbf, 0xf6, 0x6d,
|
||||
0x7b, 0xd7, 0x8f, 0x38, 0x9b, 0xb5, 0xdb, 0x3c, 0x1a, 0x67, 0x6d, 0x27,
|
||||
0x4e, 0xec, 0x38, 0xa4, 0x8e, 0x1d, 0x87, 0xc4, 0xac, 0xdb, 0x26, 0xb6,
|
||||
0x37, 0xd4, 0x09, 0x4d, 0xc7, 0xbb, 0x13, 0xef, 0x26, 0xeb, 0x99, 0x65,
|
||||
0x67, 0xd6, 0x09, 0x54, 0x04, 0x53, 0x82, 0x14, 0xa9, 0x0f, 0x55, 0xe2,
|
||||
0x55, 0x21, 0x28, 0x41, 0x0a, 0x52, 0xa9, 0x5a, 0x29, 0x3f, 0x4a, 0x55,
|
||||
0x5a, 0x7e, 0x14, 0xd4, 0x1f, 0x15, 0x14, 0x10, 0x10, 0xa9, 0x95, 0xa8,
|
||||
0xa0, 0x88, 0xa2, 0x46, 0xa8, 0x48, 0xfc, 0x00, 0x29, 0x42, 0x88, 0xf2,
|
||||
0xdd, 0x3b, 0x77, 0xd6, 0xe3, 0x8d, 0x4b, 0xda, 0xb5, 0xbf, 0x3d, 0xe7,
|
||||
0x9e, 0x73, 0xee, 0xbd, 0xe7, 0x9e, 0x7b, 0xee, 0x99, 0x99, 0x9d, 0xbc,
|
||||
0x76, 0xb1, 0x69, 0x60, 0xf7, 0x30, 0xfd, 0xbc, 0xfa, 0xb5, 0x0f, 0x7f,
|
||||
0x7c, 0xf3, 0xbf, 0x7f, 0x3c, 0x39, 0xf6, 0x98, 0xfc, 0xbd, 0x6b, 0x37,
|
||||
0xae, 0xdf, 0xd4, 0x27, 0x2e, 0x3e, 0xba, 0x2d, 0x9e, 0x6b, 0xe9, 0x24,
|
||||
0x2a, 0x13, 0xd1, 0xc5, 0xec, 0x9e, 0x28, 0x89, 0xcf, 0x5c, 0x07, 0x91,
|
||||
0x41, 0xb6, 0x7c, 0x33, 0xb0, 0x53, 0x26, 0xba, 0x1b, 0xf4, 0x59, 0xd0,
|
||||
0x66, 0x26, 0xf7, 0x10, 0x8d, 0x81, 0xbe, 0x01, 0xea, 0x07, 0xbd, 0xea,
|
||||
0x23, 0x7a, 0x1c, 0xbd, 0x57, 0xd1, 0x18, 0x08, 0x12, 0x0d, 0x01, 0xfb,
|
||||
0x80, 0x7b, 0x81, 0x49, 0xe0, 0x38, 0xf0, 0x20, 0x70, 0x1a, 0x78, 0x18,
|
||||
0x58, 0x06, 0xca, 0x40, 0x15, 0x78, 0x04, 0xf8, 0x32, 0x70, 0x19, 0xf8,
|
||||
0x3e, 0xf0, 0x0a, 0xf0, 0x2b, 0xe0, 0x6f, 0x00, 0x85, 0x30, 0x2f, 0x70,
|
||||
0x14, 0x28, 0x00, 0x57, 0x80, 0xe7, 0x80, 0xdf, 0x00, 0xff, 0x02, 0x62,
|
||||
0x61, 0xa2, 0x7e, 0x60, 0x06, 0xb8, 0x04, 0xfc, 0x08, 0xf8, 0x03, 0xd0,
|
||||
0x1c, 0xc1, 0xdc, 0x80, 0x09, 0x7c, 0x1b, 0x78, 0x15, 0x78, 0x17, 0x20,
|
||||
0xf8, 0xd8, 0x0b, 0x4c, 0x00, 0x59, 0xe0, 0x1c, 0xf0, 0x08, 0x70, 0x09,
|
||||
0x78, 0x14, 0xb8, 0x02, 0x3c, 0x01, 0x7c, 0x1d, 0xf8, 0x0e, 0xf0, 0x0c,
|
||||
0xf0, 0x43, 0xe0, 0x39, 0xe0, 0x3a, 0xf0, 0x12, 0xf0, 0x0a, 0xf0, 0x4b,
|
||||
0xe0, 0xd7, 0xc0, 0xef, 0x81, 0xb7, 0x80, 0x77, 0x80, 0xbf, 0x00, 0x7f,
|
||||
0x07, 0xfe, 0x0d, 0x44, 0x63, 0x44, 0x7d, 0xc0, 0x08, 0x70, 0x04, 0x38,
|
||||
0x03, 0xac, 0x00, 0x5f, 0x05, 0x1e, 0x03, 0x5e, 0x00, 0x5e, 0x07, 0x7e,
|
||||
0x01, 0xbc, 0x05, 0xfc, 0x19, 0x78, 0x1f, 0xb8, 0x05, 0xf8, 0x5b, 0x89,
|
||||
0xe2, 0xc0, 0x56, 0x60, 0x2f, 0x70, 0x04, 0x38, 0x05, 0x18, 0xc0, 0x2a,
|
||||
0xf0, 0x04, 0xf0, 0x03, 0xe0, 0x27, 0xc0, 0x6f, 0x81, 0xf7, 0x01, 0xb9,
|
||||
0x8d, 0xa8, 0x1d, 0xd8, 0x01, 0x4c, 0x03, 0xa7, 0x01, 0x0b, 0x78, 0x12,
|
||||
0xf8, 0x16, 0x70, 0x15, 0x78, 0x01, 0x78, 0x13, 0xf8, 0x2b, 0xf0, 0x01,
|
||||
0xe0, 0x6f, 0x47, 0x2c, 0x80, 0x03, 0x40, 0x16, 0x30, 0x00, 0x6c, 0x23,
|
||||
0x89, 0xb0, 0x13, 0xc2, 0x48, 0x22, 0x64, 0x04, 0xb7, 0x08, 0xd3, 0x10,
|
||||
0xba, 0x51, 0x1c, 0x48, 0x00, 0x48, 0x11, 0x42, 0xea, 0x50, 0x17, 0xd0,
|
||||
0x0d, 0x6c, 0x02, 0x7a, 0x44, 0xbe, 0x24, 0x81, 0x2d, 0x40, 0x0a, 0xe8,
|
||||
0x05, 0xfa, 0x80, 0xbb, 0xc8, 0xce, 0xa1, 0xad, 0xc0, 0x36, 0x60, 0x3b,
|
||||
0xb0, 0x83, 0xe5, 0x16, 0x30, 0x4a, 0x76, 0x1e, 0xb1, 0xcf, 0x48, 0x80,
|
||||
0xa8, 0x11, 0x54, 0x11, 0xed, 0x66, 0x81, 0x71, 0xc8, 0x03, 0x82, 0xcf,
|
||||
0x08, 0x9b, 0x16, 0x61, 0xd3, 0x28, 0x7c, 0x60, 0x9f, 0x26, 0xc1, 0xcf,
|
||||
0x05, 0x6c, 0x7d, 0xb7, 0x18, 0x73, 0xa7, 0xf0, 0x91, 0x7d, 0xfa, 0x05,
|
||||
0x7f, 0x1a, 0xf2, 0x5d, 0x82, 0xcf, 0x83, 0x4f, 0x0b, 0xbe, 0x0c, 0x7e,
|
||||
0x50, 0xf0, 0x17, 0x5d, 0xfc, 0x65, 0xf0, 0x03, 0x82, 0x1f, 0x77, 0xd9,
|
||||
0xb3, 0xb9, 0x86, 0x04, 0xff, 0x14, 0xf8, 0xdd, 0x82, 0x7f, 0xda, 0x25,
|
||||
0xbf, 0xe6, 0xe2, 0x9f, 0x07, 0xbf, 0x57, 0xf0, 0x2f, 0xba, 0xf8, 0xd7,
|
||||
0xc0, 0xef, 0x11, 0xfc, 0x9b, 0x2e, 0xf9, 0x0d, 0x57, 0xdf, 0x77, 0xc1,
|
||||
0x0f, 0x0b, 0xfe, 0xa6, 0x4b, 0xfe, 0x4f, 0x57, 0xdf, 0xff, 0xb8, 0xe4,
|
||||
0x9e, 0xe0, 0x9a, 0xbc, 0x25, 0xb8, 0x26, 0x1f, 0x71, 0xd9, 0xb7, 0xb2,
|
||||
0xf3, 0x2a, 0xf8, 0x6e, 0xf0, 0xfb, 0xc5, 0x5e, 0xb2, 0x75, 0x1d, 0x10,
|
||||
0x7c, 0xc6, 0xc5, 0x33, 0xdf, 0x3e, 0x25, 0xf8, 0xbe, 0xa0, 0x5d, 0x0b,
|
||||
0x7a, 0xc4, 0x98, 0x21, 0x64, 0xcb, 0x7d, 0x3c, 0x7f, 0x6c, 0x1a, 0x41,
|
||||
0x06, 0xe5, 0x38, 0x8d, 0xd1, 0x59, 0x91, 0x53, 0x4b, 0x22, 0xaf, 0x26,
|
||||
0x39, 0xb5, 0xed, 0x58, 0x8e, 0x4d, 0x73, 0xda, 0x45, 0x19, 0xe6, 0x93,
|
||||
0x90, 0xb7, 0x62, 0x07, 0x35, 0x9e, 0x0f, 0x5e, 0xca, 0xf3, 0xbd, 0x6e,
|
||||
0x20, 0x99, 0xef, 0xb3, 0x44, 0x9f, 0xe6, 0x39, 0x90, 0xa4, 0x07, 0x05,
|
||||
0x5d, 0x10, 0xf4, 0x94, 0xa0, 0xa7, 0x39, 0xed, 0xa1, 0xcf, 0x11, 0xcb,
|
||||
0x9d, 0x30, 0xef, 0x17, 0x40, 0x86, 0x4e, 0x10, 0xcb, 0x73, 0x7b, 0x9c,
|
||||
0x10, 0xb2, 0xd8, 0xa6, 0x31, 0xba, 0x9f, 0xd3, 0x3e, 0x7a, 0x80, 0xfb,
|
||||
0x6f, 0xcb, 0xc3, 0xd4, 0x4e, 0xc7, 0x39, 0x8d, 0xd3, 0x09, 0x4e, 0xdb,
|
||||
0x68, 0x8e, 0xd3, 0x56, 0x9a, 0xe7, 0x54, 0x21, 0x95, 0xd3, 0x14, 0x2d,
|
||||
0x72, 0xda, 0xcb, 0x69, 0x04, 0x27, 0x44, 0xe6, 0xb4, 0x9b, 0x66, 0x39,
|
||||
0xed, 0xe1, 0x34, 0x0a, 0x4e, 0xe6, 0x74, 0x13, 0x8d, 0x73, 0xda, 0x40,
|
||||
0x87, 0x44, 0x3c, 0x26, 0x38, 0xf5, 0xd0, 0x11, 0x4e, 0xa3, 0xf4, 0x10,
|
||||
0xa7, 0x49, 0x3a, 0xc3, 0x69, 0x82, 0x1e, 0xe6, 0xf1, 0x69, 0xe4, 0xfd,
|
||||
0x63, 0x88, 0xc0, 0x61, 0x4e, 0x43, 0x74, 0x94, 0x53, 0x99, 0x8e, 0x71,
|
||||
0xda, 0x40, 0x9f, 0xe1, 0xd4, 0x5e, 0x4f, 0x4c, 0xac, 0x27, 0x26, 0xd6,
|
||||
0xc1, 0xe8, 0x09, 0x4e, 0xed, 0x75, 0xc4, 0xc4, 0x3a, 0x62, 0x38, 0xb1,
|
||||
0x59, 0x4e, 0x7d, 0x74, 0x92, 0x53, 0x3f, 0x7d, 0x96, 0x53, 0x7b, 0x7d,
|
||||
0x31, 0x9c, 0xe0, 0x45, 0x4e, 0x9b, 0xa9, 0x28, 0xe8, 0x39, 0x4e, 0x83,
|
||||
0x74, 0x9e, 0xd3, 0x00, 0x95, 0x38, 0x6d, 0xa1, 0x65, 0xbe, 0x6f, 0x5d,
|
||||
0xdc, 0xcf, 0x56, 0xec, 0xdb, 0x14, 0xa7, 0x7e, 0x3e, 0x6e, 0x9b, 0x88,
|
||||
0x7b, 0x57, 0x2d, 0xfe, 0x84, 0x9d, 0xb4, 0xcf, 0x30, 0xfb, 0x7c, 0x88,
|
||||
0xcf, 0x9b, 0x28, 0x30, 0x5f, 0x8c, 0xdb, 0xed, 0xb0, 0xd0, 0x07, 0x5d,
|
||||
0xfa, 0x77, 0xa0, 0x7f, 0x52, 0xe8, 0x23, 0x42, 0xce, 0xea, 0xd0, 0x40,
|
||||
0xc0, 0xd6, 0x7f, 0x00, 0xfd, 0x4b, 0x42, 0x1f, 0x15, 0xfd, 0x03, 0xae,
|
||||
0xfe, 0x1e, 0x14, 0xaf, 0xdf, 0x09, 0x7d, 0x4c, 0xe8, 0xbb, 0x5c, 0xfa,
|
||||
0x56, 0xe8, 0x6f, 0x0a, 0x7d, 0xeb, 0x06, 0xfa, 0x3e, 0xe8, 0x8f, 0x24,
|
||||
0xec, 0x76, 0xdb, 0x06, 0xfa, 0xc3, 0xd0, 0x9f, 0x16, 0x7a, 0x1f, 0xd6,
|
||||
0xef, 0x15, 0x3a, 0x56, 0xe3, 0xe6, 0xb0, 0x20, 0x7d, 0x7e, 0x2b, 0x25,
|
||||
0x27, 0x9b, 0xe4, 0x10, 0x02, 0x20, 0xe3, 0x4f, 0x22, 0xc7, 0x16, 0xb5,
|
||||
0x27, 0xcc, 0x5a, 0xa7, 0x24, 0xb6, 0x62, 0x85, 0x6b, 0xd7, 0x74, 0xbc,
|
||||
0x6f, 0xf2, 0x6e, 0x64, 0x44, 0x10, 0x59, 0xa2, 0xd4, 0x6a, 0xa4, 0x3d,
|
||||
0xae, 0x44, 0xfa, 0x40, 0x9a, 0x92, 0x4a, 0x8b, 0x1c, 0x91, 0x89, 0x6b,
|
||||
0x9d, 0xbe, 0x2c, 0xc6, 0x73, 0x92, 0x44, 0x95, 0x30, 0x1b, 0xaf, 0x45,
|
||||
0xd6, 0xc3, 0x64, 0xd3, 0xe4, 0x00, 0x76, 0xbd, 0x49, 0x0a, 0x49, 0xeb,
|
||||
0xed, 0xfd, 0xc2, 0xde, 0xad, 0xbf, 0xdd, 0xcf, 0x26, 0xee, 0xa7, 0x54,
|
||||
0xe7, 0x8b, 0xaf, 0xe6, 0xcb, 0x36, 0xf8, 0xc2, 0x74, 0x9e, 0xf5, 0x3a,
|
||||
0x85, 0xe9, 0xb6, 0x53, 0x4c, 0x0e, 0x22, 0x2e, 0x4c, 0x5b, 0xd7, 0x6f,
|
||||
0x7c, 0x07, 0x62, 0xf3, 0x51, 0xba, 0x7b, 0x84, 0xce, 0x5b, 0xf3, 0xc5,
|
||||
0xf6, 0x42, 0xa6, 0x53, 0xb2, 0x42, 0x0b, 0x8a, 0x07, 0x7e, 0xb1, 0xea,
|
||||
0x15, 0xe4, 0xfe, 0xba, 0x63, 0xb7, 0x59, 0xea, 0xc6, 0x7a, 0x12, 0xc8,
|
||||
0x27, 0xd6, 0x5f, 0xae, 0x8d, 0xdd, 0xc0, 0xc6, 0x1e, 0x50, 0x68, 0x76,
|
||||
0xd0, 0x83, 0xf1, 0x7b, 0x29, 0x3c, 0x11, 0x84, 0xbd, 0x5c, 0xdb, 0x33,
|
||||
0x96, 0x3f, 0xb3, 0xe3, 0x5e, 0x6a, 0x97, 0x25, 0x7a, 0x55, 0x5e, 0x98,
|
||||
0xf0, 0xd2, 0xdc, 0x84, 0x8f, 0x52, 0x4a, 0x84, 0xca, 0xc7, 0xdb, 0x69,
|
||||
0x52, 0x36, 0x06, 0x1a, 0xb1, 0xa6, 0x30, 0xc6, 0xf1, 0xd5, 0xd6, 0xc9,
|
||||
0xea, 0x52, 0x39, 0xd9, 0x80, 0x5a, 0x8b, 0x8b, 0x2a, 0x2d, 0x24, 0xbd,
|
||||
0xb0, 0x8f, 0x51, 0x79, 0x60, 0x0b, 0x4d, 0x7a, 0x4f, 0xf5, 0xfa, 0x48,
|
||||
0x0f, 0x6f, 0x62, 0xf1, 0x57, 0xa2, 0xad, 0x3a, 0x74, 0x93, 0xf0, 0x4b,
|
||||
0xc1, 0x77, 0xd4, 0xab, 0x27, 0x3d, 0xa8, 0x11, 0x7a, 0xd2, 0xc7, 0x65,
|
||||
0x7e, 0xd4, 0xcd, 0x94, 0x12, 0xa2, 0xb9, 0xbb, 0x7c, 0x18, 0xad, 0x99,
|
||||
0x6b, 0x7a, 0x30, 0xe6, 0xe7, 0xc3, 0x9b, 0xe1, 0x7d, 0x0a, 0x73, 0xce,
|
||||
0xf5, 0x31, 0x4d, 0x90, 0x5b, 0x27, 0xa1, 0x49, 0x29, 0xad, 0xb6, 0x6c,
|
||||
0x60, 0x2f, 0x4d, 0xfa, 0x3b, 0x3d, 0x07, 0x11, 0xe9, 0x4e, 0x1a, 0x62,
|
||||
0x97, 0x00, 0xbe, 0x7b, 0xee, 0xfd, 0x9b, 0x0b, 0x33, 0x3f, 0xee, 0x22,
|
||||
0xbb, 0x12, 0x2a, 0xb5, 0x88, 0xb2, 0xeb, 0xf6, 0xdc, 0x03, 0x0d, 0xa8,
|
||||
0x4e, 0xb3, 0xc7, 0xfd, 0xd4, 0x8e, 0x25, 0x0d, 0x4a, 0x92, 0x32, 0x22,
|
||||
0x45, 0x79, 0x9b, 0x65, 0x90, 0x9f, 0x5a, 0xfc, 0x2c, 0x83, 0x18, 0x2d,
|
||||
0x9f, 0xec, 0xa3, 0xb6, 0xc5, 0x16, 0x5f, 0x0f, 0x45, 0x80, 0xa8, 0x2f,
|
||||
0xe2, 0x0b, 0x80, 0x6e, 0xf5, 0xd9, 0xd7, 0xef, 0x4d, 0x7c, 0xce, 0x1e,
|
||||
0x58, 0x7a, 0x6b, 0xf1, 0x66, 0xbe, 0xcc, 0xf6, 0xf9, 0x79, 0x64, 0x06,
|
||||
0x7d, 0x1e, 0xc4, 0x77, 0xd4, 0xa7, 0x20, 0x43, 0xcb, 0xe3, 0x43, 0x34,
|
||||
0x31, 0x1c, 0xe2, 0xb1, 0x77, 0x6c, 0x23, 0x3c, 0xf6, 0xb0, 0xc5, 0x46,
|
||||
0x0e, 0x62, 0x3b, 0x47, 0x65, 0xd8, 0x7b, 0x43, 0xde, 0xf2, 0xf8, 0x4e,
|
||||
0xf2, 0x1c, 0x6f, 0xf2, 0xfe, 0xc9, 0xeb, 0x95, 0x42, 0x5e, 0xe6, 0x09,
|
||||
0xeb, 0x63, 0xef, 0x76, 0xcf, 0xed, 0xfe, 0x2b, 0xf0, 0x1f, 0x99, 0x58,
|
||||
0x1e, 0xef, 0xe7, 0xbe, 0x8e, 0xfa, 0x7c, 0xd4, 0xe9, 0x63, 0x96, 0x11,
|
||||
0xe1, 0x37, 0x6b, 0x39, 0x9e, 0x33, 0xbf, 0xdb, 0xe0, 0x35, 0xbb, 0x6f,
|
||||
0x90, 0xf8, 0x5f, 0x17, 0xcf, 0x60, 0x27, 0x76, 0x01, 0x9e, 0xfb, 0xfb,
|
||||
0xd1, 0x4e, 0x41, 0x63, 0x67, 0xdb, 0xa9, 0x70, 0x03, 0x2d, 0x44, 0xfd,
|
||||
0x7c, 0x6d, 0x4d, 0xb0, 0xf6, 0x09, 0x5b, 0x56, 0xbb, 0x66, 0x55, 0xe1,
|
||||
0x05, 0x49, 0xca, 0x28, 0xc6, 0x09, 0x92, 0xff, 0x61, 0x8f, 0xe9, 0xff,
|
||||
0xe6, 0x8a, 0x2f, 0xc5, 0xa3, 0xe1, 0xe1, 0x23, 0xdb, 0xa3, 0xb7, 0xf0,
|
||||
0x5c, 0x64, 0xbe, 0xb7, 0x43, 0xb0, 0x30, 0xe8, 0x87, 0x8f, 0x41, 0x0a,
|
||||
0x48, 0xf0, 0x4b, 0xf2, 0xf2, 0x7d, 0x63, 0xfe, 0x34, 0xae, 0x8b, 0x51,
|
||||
0xe0, 0xb6, 0x18, 0xb1, 0x39, 0xca, 0xe3, 0x83, 0x88, 0x50, 0x90, 0x9f,
|
||||
0x65, 0xa5, 0x96, 0xcb, 0x1f, 0x61, 0x9b, 0xdd, 0xfd, 0x09, 0x6c, 0xfb,
|
||||
0x84, 0xad, 0x7b, 0x9d, 0x91, 0x8f, 0x58, 0xa7, 0xdf, 0xf4, 0x5f, 0x66,
|
||||
0x2b, 0xdd, 0x23, 0xf6, 0xfd, 0x93, 0xf4, 0x19, 0x16, 0x7d, 0xd6, 0xaa,
|
||||
0x8e, 0x9d, 0x53, 0x73, 0xa8, 0x3b, 0x11, 0xce, 0xcb, 0xeb, 0xeb, 0x55,
|
||||
0xb8, 0x11, 0xa7, 0x60, 0x17, 0x6a, 0xe3, 0x5a, 0xfd, 0x73, 0x7a, 0x36,
|
||||
0x88, 0x3d, 0x63, 0x95, 0xa2, 0x11, 0x95, 0xa2, 0x09, 0x23, 0x4b, 0xae,
|
||||
0xf3, 0xe0, 0xa9, 0xe9, 0x83, 0x5c, 0xd6, 0x20, 0x76, 0xa4, 0x9b, 0x9c,
|
||||
0xfb, 0xd0, 0x00, 0xe7, 0xed, 0xfb, 0x87, 0x26, 0x31, 0x6f, 0x77, 0xad,
|
||||
0xbf, 0x73, 0xee, 0x1d, 0x1b, 0x66, 0xe5, 0x11, 0xd4, 0x5f, 0xab, 0x63,
|
||||
0xac, 0x67, 0x73, 0x6d, 0xb7, 0x3d, 0xb5, 0x39, 0xba, 0x6b, 0xd7, 0x3f,
|
||||
0xe7, 0x3a, 0xa5, 0xd4, 0x7a, 0x3a, 0x7c, 0x13, 0xbf, 0xef, 0x95, 0x85,
|
||||
0x27, 0x8a, 0xa0, 0x32, 0xad, 0x5d, 0x1b, 0x99, 0x2c, 0xca, 0xaf, 0x66,
|
||||
0xb6, 0x2c, 0x26, 0xec, 0x63, 0x62, 0xfc, 0x2e, 0xd1, 0xee, 0x12, 0xfd,
|
||||
0xba, 0xb1, 0x4a, 0x26, 0xef, 0x11, 0xbe, 0x04, 0xc0, 0xf5, 0xf0, 0xdd,
|
||||
0x57, 0xb6, 0x6d, 0xcf, 0x92, 0xef, 0x40, 0x51, 0x2f, 0x5a, 0x07, 0x49,
|
||||
0x3e, 0x38, 0x46, 0x2d, 0x93, 0x85, 0x8a, 0xb1, 0xac, 0x4d, 0x96, 0x8a,
|
||||
0x9a, 0x6e, 0x91, 0x4f, 0x50, 0x69, 0x9a, 0xe4, 0xe9, 0x0c, 0x35, 0x4f,
|
||||
0x57, 0x73, 0xda, 0x49, 0x6d, 0x31, 0x5b, 0xd4, 0x2e, 0x90, 0x94, 0x21,
|
||||
0x25, 0x03, 0xa1, 0x07, 0x5f, 0x19, 0x92, 0x33, 0xac, 0x89, 0xaf, 0x8e,
|
||||
0x8c, 0xaa, 0xe7, 0x2b, 0x46, 0x31, 0x9f, 0xce, 0x19, 0xba, 0x85, 0xce,
|
||||
0xe9, 0x49, 0x46, 0x2f, 0x5a, 0x63, 0x2e, 0xd5, 0x52, 0x45, 0x2d, 0x17,
|
||||
0x8a, 0x39, 0x33, 0x3d, 0x51, 0xb4, 0x96, 0xd5, 0xf2, 0x18, 0x45, 0x6a,
|
||||
0x2a, 0x5d, 0xb3, 0xd2, 0xf3, 0x95, 0xe2, 0x18, 0x75, 0xad, 0x13, 0x15,
|
||||
0x2c, 0xab, 0x9c, 0x9e, 0x35, 0x4b, 0x53, 0x95, 0x8a, 0x51, 0x19, 0xa3,
|
||||
0xd6, 0x9a, 0xd2, 0x30, 0xd3, 0x33, 0x9a, 0x69, 0xaa, 0x4b, 0xda, 0x18,
|
||||
0xf5, 0xd5, 0xa4, 0x17, 0xb4, 0xc5, 0xf3, 0x45, 0x2b, 0x3d, 0xad, 0xae,
|
||||
0xa8, 0x66, 0xae, 0x52, 0x2c, 0x5b, 0xc7, 0xe0, 0x43, 0xe5, 0xac, 0x9a,
|
||||
0x83, 0x55, 0xb2, 0xde, 0xca, 0x19, 0xf6, 0x28, 0xe4, 0x25, 0x0d, 0xa3,
|
||||
0xf7, 0xd4, 0x5b, 0x64, 0xd5, 0x52, 0x55, 0x9b, 0x54, 0x4b, 0xa5, 0x45,
|
||||
0x35, 0x77, 0xfe, 0x4e, 0xfa, 0x03, 0xb7, 0xcf, 0x80, 0x98, 0xb9, 0xe3,
|
||||
0x3a, 0x46, 0xbd, 0x1b, 0x58, 0x9c, 0xd0, 0x4c, 0xa3, 0x5a, 0xc9, 0x69,
|
||||
0x27, 0xb4, 0xcf, 0x57, 0x35, 0xd3, 0xda, 0x60, 0x39, 0xeb, 0x8c, 0xcc,
|
||||
0xb2, 0xa1, 0x9b, 0x58, 0x4e, 0xf7, 0x06, 0x56, 0xb3, 0x9a, 0x65, 0x15,
|
||||
0xf5, 0x25, 0x73, 0x8c, 0x12, 0x1b, 0x68, 0xd9, 0xf6, 0x6d, 0xb0, 0x08,
|
||||
0xa1, 0x71, 0x5c, 0x4c, 0x67, 0x72, 0xc6, 0x72, 0xba, 0xb2, 0x6c, 0x96,
|
||||
0xd2, 0xe7, 0xb0, 0xed, 0x69, 0xd7, 0xde, 0x4f, 0x96, 0x54, 0xd3, 0xd4,
|
||||
0xcc, 0xbe, 0xf5, 0x6b, 0xda, 0xfe, 0x31, 0x3a, 0x08, 0xd3, 0x81, 0x3b,
|
||||
0x9b, 0xba, 0x44, 0x7d, 0x83, 0x63, 0xb4, 0xeb, 0x13, 0xf5, 0x18, 0xa3,
|
||||
0xa1, 0x3b, 0xdb, 0xdf, 0xa7, 0x5a, 0xc5, 0x15, 0xcd, 0x95, 0x18, 0x1f,
|
||||
0xa3, 0x0f, 0x9a, 0x87, 0xca, 0x65, 0x57, 0x9f, 0xd4, 0x1d, 0xfb, 0x30,
|
||||
0x9b, 0xbc, 0x5a, 0x5a, 0x29, 0x9e, 0x4f, 0xab, 0xba, 0x6e, 0x58, 0x98,
|
||||
0xd5, 0xd0, 0xd3, 0x53, 0x7a, 0xae, 0x64, 0x98, 0xd8, 0x23, 0x6e, 0xc5,
|
||||
0x12, 0xe2, 0xff, 0xd8, 0xcc, 0x68, 0x56, 0xc1, 0xc8, 0xb3, 0x2d, 0xbb,
|
||||
0xdd, 0xe8, 0x98, 0xae, 0x6b, 0x15, 0x31, 0xc8, 0x96, 0x0d, 0xf4, 0x33,
|
||||
0xda, 0xf2, 0xa2, 0x30, 0x60, 0xbe, 0x6c, 0xda, 0xc0, 0x64, 0xb6, 0xb8,
|
||||
0xa4, 0xab, 0x56, 0xb5, 0x82, 0xe5, 0x44, 0x33, 0xe7, 0x70, 0x64, 0xd2,
|
||||
0x25, 0x55, 0x5f, 0x4a, 0xdf, 0xbf, 0x78, 0x4e, 0xcb, 0x59, 0xeb, 0x65,
|
||||
0xb3, 0x56, 0x05, 0xfe, 0x8c, 0x51, 0xa8, 0x2e, 0x78, 0x24, 0x65, 0x49,
|
||||
0xce, 0x4e, 0x93, 0x27, 0x3b, 0x8d, 0x92, 0xa0, 0x64, 0x59, 0x5d, 0xc8,
|
||||
0xb2, 0xba, 0xe0, 0x65, 0xdf, 0x28, 0x0f, 0x59, 0xc6, 0xf2, 0x4a, 0xa1,
|
||||
0x64, 0x33, 0x4c, 0x99, 0xc9, 0x4c, 0xf3, 0x6f, 0xa6, 0x5b, 0xa0, 0x50,
|
||||
0x5d, 0x60, 0x49, 0x5a, 0x20, 0x65, 0x01, 0xa3, 0xc8, 0x0b, 0xe8, 0xb1,
|
||||
0xc0, 0x06, 0x5a, 0xc8, 0x2c, 0x80, 0x6f, 0x38, 0x73, 0x66, 0x7a, 0x7e,
|
||||
0x72, 0xea, 0xcc, 0x19, 0x6a, 0x56, 0x73, 0x39, 0x1c, 0xfc, 0x23, 0x25,
|
||||
0x75, 0xc9, 0xa4, 0x76, 0x35, 0x9f, 0xdf, 0xe0, 0xb0, 0x53, 0x38, 0x5f,
|
||||
0x34, 0x51, 0x87, 0x74, 0x2c, 0xc4, 0xf6, 0x98, 0xa2, 0xda, 0x0a, 0x0e,
|
||||
0xab, 0x6a, 0x69, 0x6b, 0xe6, 0xd4, 0x22, 0x64, 0x08, 0xc6, 0xb1, 0x3c,
|
||||
0xc5, 0xd7, 0x5a, 0x38, 0x65, 0xd5, 0x92, 0x25, 0xea, 0x02, 0xb5, 0x2d,
|
||||
0x69, 0xd6, 0x21, 0xfb, 0xcc, 0xcc, 0x9b, 0x5a, 0x65, 0x96, 0x77, 0x36,
|
||||
0xa9, 0x19, 0x62, 0xe7, 0xc0, 0x91, 0x0f, 0x8d, 0xf9, 0x4a, 0x89, 0x92,
|
||||
0x05, 0xde, 0x69, 0x6d, 0x92, 0xa9, 0xba, 0x41, 0xa9, 0xcd, 0xb6, 0xa8,
|
||||
0x3b, 0xf5, 0xe4, 0x29, 0x18, 0xf8, 0x6e, 0x66, 0xdf, 0x87, 0xb5, 0x92,
|
||||
0x66, 0x69, 0x79, 0x6a, 0x60, 0x8d, 0x8c, 0x91, 0x3b, 0x4f, 0x1e, 0x5d,
|
||||
0x5d, 0xd6, 0x28, 0xa4, 0xd7, 0x05, 0x3f, 0x60, 0xe8, 0x93, 0xc8, 0x14,
|
||||
0xed, 0x64, 0x51, 0xcf, 0x1b, 0x17, 0x28, 0x88, 0x66, 0x45, 0xc3, 0x12,
|
||||
0xd7, 0xda, 0x0f, 0xa0, 0x3e, 0x1e, 0x41, 0x95, 0x37, 0x0b, 0x18, 0x2f,
|
||||
0x60, 0xb7, 0x67, 0x2d, 0xb5, 0xc2, 0x86, 0x0f, 0x32, 0x97, 0x72, 0x1a,
|
||||
0x46, 0xe4, 0x65, 0x8c, 0x62, 0xb5, 0x76, 0xfe, 0x28, 0xaa, 0x2e, 0x2f,
|
||||
0x8d, 0x14, 0x5d, 0x13, 0x3a, 0xe5, 0x92, 0x42, 0x65, 0x8c, 0x72, 0x68,
|
||||
0xd1, 0xa8, 0x5a, 0x73, 0x46, 0xc6, 0x50, 0xf3, 0xd4, 0x5c, 0x86, 0xa3,
|
||||
0xa2, 0x1a, 0x53, 0xd4, 0xd5, 0x70, 0x22, 0xd8, 0x6e, 0x6a, 0xd6, 0x44,
|
||||
0xb5, 0x58, 0xc2, 0x0e, 0x2d, 0x18, 0xc6, 0x32, 0xbb, 0x32, 0x54, 0x8c,
|
||||
0x92, 0xc9, 0xe5, 0x87, 0x8b, 0x66, 0xb9, 0xa4, 0x7e, 0x61, 0x9d, 0xbc,
|
||||
0x15, 0x72, 0x16, 0x42, 0x3b, 0xd4, 0x53, 0xba, 0xba, 0x58, 0x82, 0xc3,
|
||||
0x1d, 0x90, 0xce, 0x56, 0xcb, 0x65, 0xa3, 0x62, 0xcd, 0x20, 0x92, 0xc5,
|
||||
0x72, 0x49, 0xac, 0xd4, 0xa4, 0x28, 0x54, 0x6c, 0x6f, 0x0e, 0x2d, 0xa1,
|
||||
0xce, 0xd8, 0xc9, 0xca, 0x65, 0x75, 0x05, 0x98, 0x7a, 0x84, 0xcc, 0xbe,
|
||||
0x44, 0x99, 0x87, 0xb5, 0xc5, 0xea, 0xd2, 0x12, 0x8c, 0x9d, 0x29, 0xc2,
|
||||
0xb6, 0x7e, 0xad, 0x1e, 0xc2, 0xc5, 0x82, 0x51, 0x2d, 0xe5, 0x79, 0xd0,
|
||||
0x73, 0x5a, 0xd9, 0x72, 0xf6, 0x2b, 0x61, 0xcb, 0xef, 0x5f, 0xd1, 0x2a,
|
||||
0x95, 0x62, 0x5e, 0xc3, 0xde, 0xb3, 0x50, 0xb0, 0x79, 0x7d, 0x56, 0xa1,
|
||||
0x68, 0xf6, 0x0d, 0x50, 0x83, 0x65, 0x08, 0x4f, 0x9a, 0xab, 0xae, 0xb4,
|
||||
0xd9, 0x84, 0x84, 0xe8, 0xb3, 0x73, 0xb8, 0x88, 0x39, 0xeb, 0x0f, 0x54,
|
||||
0x9c, 0xa9, 0x73, 0xd5, 0x4a, 0x05, 0x93, 0x4f, 0xb9, 0xd3, 0xd3, 0xbb,
|
||||
0xc2, 0x77, 0xa9, 0xe3, 0x82, 0xe3, 0x5e, 0x6d, 0xe3, 0x1d, 0x97, 0x3a,
|
||||
0x1d, 0x95, 0x2b, 0x09, 0x1c, 0x5d, 0x5c, 0xe8, 0x58, 0x02, 0x30, 0x47,
|
||||
0x6b, 0x49, 0x91, 0x10, 0x8a, 0xdb, 0xb7, 0x3e, 0x5e, 0xa7, 0xa9, 0xed,
|
||||
0xff, 0x33, 0xd2, 0xa5, 0x4b, 0x87, 0x47, 0x1e, 0x49, 0xb1, 0x8b, 0x9e,
|
||||
0xa6, 0xe7, 0x53, 0xfb, 0x53, 0x79, 0xed, 0x62, 0x6a, 0x67, 0x0a, 0x15,
|
||||
0xb1, 0x5c, 0x2c, 0x71, 0x7f, 0xfb, 0x97, 0x8d, 0xbc, 0x06, 0x45, 0x05,
|
||||
0xa9, 0xac, 0x9a, 0x1a, 0x94, 0x05, 0xd5, 0xec, 0xcf, 0x15, 0xb4, 0xdc,
|
||||
0x79, 0xb3, 0xba, 0x6c, 0xa6, 0xf6, 0x9f, 0x55, 0x4b, 0xa6, 0xb6, 0x33,
|
||||
0xb5, 0x5c, 0xd4, 0xfb, 0xd5, 0x72, 0x31, 0xb5, 0x7f, 0x68, 0xf7, 0xce,
|
||||
0x94, 0x59, 0x50, 0xfb, 0x07, 0xd1, 0x89, 0xc5, 0x21, 0x7f, 0x76, 0xcf,
|
||||
0xe0, 0xe2, 0x62, 0x7e, 0x68, 0x64, 0x71, 0x78, 0xef, 0xee, 0xfc, 0xa0,
|
||||
0x36, 0xaa, 0x0d, 0x8c, 0xe6, 0x86, 0xcf, 0xee, 0x1b, 0xca, 0xef, 0xcb,
|
||||
0x0d, 0xe7, 0xd4, 0xe1, 0xd1, 0x91, 0xfc, 0x30, 0x46, 0x45, 0xf4, 0x4d,
|
||||
0x4c, 0x87, 0x4e, 0x23, 0xbb, 0x86, 0x76, 0x0d, 0xf5, 0xe7, 0xb5, 0x95,
|
||||
0xd4, 0x97, 0x48, 0x6e, 0x97, 0x0a, 0x89, 0x36, 0x39, 0x21, 0xdf, 0xeb,
|
||||
0x69, 0x9c, 0x89, 0x7b, 0x1c, 0xc6, 0x2b, 0x77, 0x4a, 0x85, 0x6e, 0x29,
|
||||
0x2e, 0xc9, 0x71, 0xa9, 0xd0, 0xd9, 0xcd, 0xc5, 0x34, 0xd3, 0xc3, 0xa5,
|
||||
0x9e, 0xf8, 0xf6, 0x78, 0x7b, 0x7c, 0x47, 0x5c, 0x71, 0x8c, 0x1b, 0x1c,
|
||||
0xe6, 0x1e, 0x09, 0xb7, 0xe2, 0x82, 0x1f, 0x91, 0x3b, 0x60, 0xeb, 0x4d,
|
||||
0x44, 0x13, 0xad, 0x89, 0x50, 0x22, 0x9c, 0x88, 0x91, 0x84, 0xc7, 0x52,
|
||||
0xb9, 0xf9, 0x2b, 0xab, 0x9e, 0xab, 0xa1, 0x16, 0xe9, 0xa7, 0x21, 0x49,
|
||||
0x5a, 0x0d, 0x41, 0xe6, 0x97, 0xe4, 0x20, 0x64, 0x6f, 0x84, 0x43, 0xd2,
|
||||
0x7b, 0x61, 0xc8, 0x22, 0x92, 0xf4, 0x5d, 0xe0, 0x65, 0xe0, 0x6d, 0xd8,
|
||||
0xdc, 0x02, 0x9e, 0x0d, 0x93, 0x22, 0xc9, 0xf2, 0x53, 0x49, 0x89, 0xff,
|
||||
0xb7, 0xaf, 0xae, 0x7a, 0x6e, 0x44, 0xe2, 0x3f, 0x4b, 0xde, 0x82, 0xcd,
|
||||
0x37, 0xa2, 0x24, 0x4b, 0xb2, 0x17, 0xe8, 0xc0, 0x28, 0x37, 0xa3, 0x5d,
|
||||
0xd2, 0x95, 0x56, 0x49, 0x7a, 0x11, 0xd2, 0xa6, 0x66, 0xbf, 0x2c, 0x45,
|
||||
0xb7, 0x40, 0xfa, 0x7a, 0xbb, 0xf4, 0x28, 0xa6, 0x06, 0x1a, 0x00, 0xc6,
|
||||
0x7b, 0x04, 0x75, 0xa3, 0x57, 0xfa, 0x07, 0x1e, 0x6a, 0xaf, 0xc6, 0x25,
|
||||
0xe9, 0x06, 0x70, 0xb9, 0x0d, 0xbe, 0x24, 0x24, 0xe9, 0x79, 0xe0, 0x6d,
|
||||
0xe0, 0xf1, 0x0e, 0x45, 0x7a, 0x0f, 0xb2, 0x6b, 0x18, 0xfd, 0x7a, 0x1b,
|
||||
0x9b, 0xb3, 0x11, 0x4f, 0xc0, 0x7b, 0x31, 0xfa, 0xad, 0x8e, 0x7d, 0xd2,
|
||||
0x6b, 0x1d, 0xb0, 0xea, 0x60, 0x77, 0x9a, 0xa3, 0x90, 0x3c, 0xdd, 0x69,
|
||||
0xdf, 0x5b, 0x86, 0xe3, 0x44, 0xed, 0xf1, 0x35, 0x7e, 0x73, 0xdc, 0xbe,
|
||||
0x17, 0xdd, 0x16, 0xb7, 0xef, 0x4f, 0x07, 0x40, 0xf7, 0x02, 0x07, 0x5d,
|
||||
0x36, 0x19, 0x17, 0x3f, 0x2f, 0xec, 0x1f, 0x72, 0xc9, 0x54, 0x21, 0x3b,
|
||||
0x07, 0x5a, 0x48, 0xd0, 0xba, 0x8f, 0x55, 0xd7, 0xbe, 0x22, 0xda, 0xce,
|
||||
0x7d, 0x39, 0x7b, 0xc6, 0x5c, 0x85, 0xec, 0xe9, 0x3a, 0xbb, 0x6b, 0x75,
|
||||
0xed, 0x97, 0x45, 0xdb, 0x79, 0x9e, 0x60, 0xbf, 0x85, 0x5e, 0x87, 0x6c,
|
||||
0x44, 0xd0, 0xd7, 0xeb, 0xec, 0x43, 0xb4, 0x7e, 0x1e, 0x46, 0x9d, 0xf7,
|
||||
0x25, 0x6c, 0x0c, 0xe7, 0x9d, 0x09, 0x5b, 0xb3, 0xf3, 0xde, 0x84, 0xdd,
|
||||
0x63, 0x3b, 0xef, 0x4e, 0xd8, 0x5d, 0xb6, 0xf3, 0xfe, 0x84, 0x3d, 0x0f,
|
||||
0x39, 0xef, 0x50, 0xa4, 0xa4, 0xfd, 0xfc, 0xcd, 0xde, 0xa3, 0x48, 0xe1,
|
||||
0xb5, 0xdf, 0xb4, 0xe4, 0xa4, 0x3d, 0x3e, 0x7b, 0xb7, 0xe2, 0x49, 0xda,
|
||||
0xcf, 0x89, 0x2c, 0x36, 0xec, 0x87, 0x76, 0xd6, 0x97, 0xfd, 0x56, 0xa6,
|
||||
0x84, 0xed, 0x67, 0x08, 0x16, 0x23, 0x9f, 0x90, 0xb3, 0xdf, 0xd1, 0xd8,
|
||||
0x43, 0x28, 0xf3, 0x8f, 0xbd, 0xd3, 0xf9, 0x1f, 0x8b, 0x9b, 0x02, 0xf1,
|
||||
0x0c, 0x1a, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
|
||||
METHOD (constructor, "<init>", "(Landroid/content/Context;JLjava/lang/String;Ljava/lang/String;)V") \
|
||||
METHOD (canGoBack, "canGoBack", "()Z") \
|
||||
METHOD (goBack, "goBack", "()V") \
|
||||
METHOD (goForward, "goForward", "()V") \
|
||||
METHOD (loadDataWithBaseURL, "loadDataWithBaseURL", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V") \
|
||||
METHOD (loadUrl, "loadUrl", "(Ljava/lang/String;Ljava/util/Map;)V") \
|
||||
METHOD (postUrl, "postUrl", "(Ljava/lang/String;[B)V") \
|
||||
METHOD (reload, "reload", "()V") \
|
||||
METHOD (evaluateJavascript, "evaluateJavascript", "(Ljava/lang/String;)J") \
|
||||
METHOD (stopLoading, "stopLoading", "()V") \
|
||||
METHOD (disconnectNative, "disconnectNative", "()V")
|
||||
|
||||
DECLARE_JNI_CLASS_WITH_BYTECODE (JuceWebView, "com/rmsl/juce/JuceWebViewClasses$JuceWebView", 16, juceWebViewClasses)
|
||||
#undef JNI_CLASS_MEMBERS
|
||||
|
||||
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
|
||||
STATICMETHOD (getInstance, "getInstance", "()Landroid/webkit/CookieManager;")
|
||||
|
||||
DECLARE_JNI_CLASS (AndroidCookieManager, "android/webkit/CookieManager")
|
||||
#undef JNI_CLASS_MEMBERS
|
||||
|
||||
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
|
||||
METHOD (toString, "toString", "()Ljava/lang/String;")
|
||||
|
||||
DECLARE_JNI_CLASS (SslError, "android/net/http/SslError")
|
||||
#undef JNI_CLASS_MEMBERS
|
||||
|
||||
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
|
||||
STATICMETHOD (encode, "encode", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;")
|
||||
|
||||
DECLARE_JNI_CLASS (URLEncoder, "java/net/URLEncoder")
|
||||
#undef JNI_CLASS_MEMBERS
|
||||
|
||||
class MessageThreadExecutor : private AsyncUpdater
|
||||
{
|
||||
public:
|
||||
MessageThreadExecutor() = default;
|
||||
|
||||
~MessageThreadExecutor() override
|
||||
{
|
||||
cancelPendingUpdate();
|
||||
}
|
||||
|
||||
void execute (std::function<void()> fn)
|
||||
{
|
||||
const std::lock_guard lock { functionsMutex };
|
||||
functions.emplace_back (std::move (fn));
|
||||
triggerAsyncUpdate();
|
||||
}
|
||||
|
||||
private:
|
||||
void handleAsyncUpdate() override
|
||||
{
|
||||
const auto popFront = [&]() -> std::function<void()>
|
||||
{
|
||||
const std::lock_guard lock { functionsMutex };
|
||||
|
||||
if (functions.empty())
|
||||
return nullptr;
|
||||
|
||||
const ScopeGuard scope { [&] { functions.pop_front(); } };
|
||||
|
||||
return functions.front();
|
||||
};
|
||||
|
||||
for (auto fn = popFront(); fn != nullptr; fn = popFront())
|
||||
fn();
|
||||
}
|
||||
|
||||
std::deque<std::function<void()>> functions;
|
||||
std::mutex functionsMutex;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE (MessageThreadExecutor)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
struct WebBrowserComponent::Impl::Platform : public AndroidViewComponent,
|
||||
public PlatformInterface,
|
||||
public AsyncUpdater
|
||||
{
|
||||
public:
|
||||
Platform (WebBrowserComponent& o,
|
||||
const WebBrowserComponent::Options& browserOptions,
|
||||
const StringArray& userScripts)
|
||||
: owner (o)
|
||||
{
|
||||
auto* env = getEnv();
|
||||
|
||||
setView (env->NewObject (JuceWebView,
|
||||
JuceWebView.constructor,
|
||||
getMainActivity().get(),
|
||||
reinterpret_cast<jlong> (this),
|
||||
javaString (browserOptions.getUserAgent()).get(),
|
||||
javaString (userScripts.joinIntoString ("\n")).get()));
|
||||
|
||||
owner.addAndMakeVisible (this);
|
||||
}
|
||||
|
||||
void checkWindowAssociation() override
|
||||
{
|
||||
if (owner.isShowing())
|
||||
{
|
||||
if (owner.blankPageShown)
|
||||
owner.goBack();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (owner.unloadPageWhenHidden && ! owner.blankPageShown)
|
||||
{
|
||||
// when the component becomes invisible, some stuff like flash
|
||||
// carries on playing audio, so we need to force it onto a blank
|
||||
// page to avoid this, (and send it back when it's made visible again).
|
||||
|
||||
owner.blankPageShown = true;
|
||||
goToURL ("about:blank", nullptr, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setWebViewSize (int width, int height) override
|
||||
{
|
||||
setSize (width, height);
|
||||
}
|
||||
|
||||
void evaluateJavascript (const String& script, WebBrowserComponent::EvaluationCallback callback) override
|
||||
{
|
||||
JUCE_ASSERT_MESSAGE_THREAD
|
||||
|
||||
auto* env = getEnv();
|
||||
auto view = (jobject) getView();
|
||||
|
||||
const auto resultId = (int64) env->CallLongMethod (view,
|
||||
JuceWebView.evaluateJavascript,
|
||||
javaString (script).get());
|
||||
|
||||
if (callback != nullptr)
|
||||
evaluationCallbacks[resultId] = std::move (callback);
|
||||
}
|
||||
|
||||
~Platform() override
|
||||
{
|
||||
auto* env = getEnv();
|
||||
|
||||
env->CallVoidMethod ((jobject) getView(), JuceWebView.stopLoading);
|
||||
|
||||
masterReference.clear();
|
||||
|
||||
// if other Java thread is waiting for us to respond to page load request
|
||||
// wake it up immediately (false answer will be sent), so that it releases
|
||||
// the lock we need when calling hostDeleted.
|
||||
responseReadyEvent.signal();
|
||||
|
||||
resourceRequester.cancelAllResourceRequests();
|
||||
|
||||
env->CallVoidMethod ((jobject) getView(), JuceWebView.disconnectNative);
|
||||
}
|
||||
|
||||
void goToURL (const String& url,
|
||||
const StringArray* headers,
|
||||
const MemoryBlock* postData) override
|
||||
{
|
||||
auto* env = getEnv();
|
||||
|
||||
if (headers == nullptr && postData == nullptr)
|
||||
{
|
||||
env->CallVoidMethod ((jobject) getView(), JuceWebView.loadUrl, javaString (url).get(), 0);
|
||||
}
|
||||
else if (headers != nullptr && postData == nullptr)
|
||||
{
|
||||
auto headersMap = LocalRef<jobject> (env->NewObject (JavaHashMap,
|
||||
JavaHashMap.constructorWithCapacity,
|
||||
headers->size()));
|
||||
|
||||
for (const auto& header : *headers)
|
||||
{
|
||||
auto name = header.upToFirstOccurrenceOf (":", false, false).trim();
|
||||
auto value = header.fromFirstOccurrenceOf (":", false, false).trim();
|
||||
|
||||
env->CallObjectMethod (headersMap, JavaMap.put,
|
||||
javaString (name).get(),
|
||||
javaString (value).get());
|
||||
}
|
||||
|
||||
env->CallVoidMethod ((jobject) getView(), JuceWebView.loadUrl,
|
||||
javaString (url).get(), headersMap.get());
|
||||
}
|
||||
else if (headers == nullptr && postData != nullptr)
|
||||
{
|
||||
auto dataStringJuce = postData->toString();
|
||||
auto dataStringJava = javaString (dataStringJuce);
|
||||
auto encodingString = LocalRef<jobject> (env->CallStaticObjectMethod (URLEncoder, URLEncoder.encode,
|
||||
dataStringJava.get(), javaString ("utf-8").get()));
|
||||
|
||||
auto bytes = LocalRef<jbyteArray> ((jbyteArray) env->CallObjectMethod (encodingString, JavaString.getBytes));
|
||||
|
||||
env->CallVoidMethod ((jobject) getView(), JuceWebView.postUrl,
|
||||
javaString (url).get(), bytes.get());
|
||||
}
|
||||
else if (headers != nullptr && postData != nullptr)
|
||||
{
|
||||
// There is no support for both extra headers and post data in Android WebView, so
|
||||
// we need to open URL manually.
|
||||
|
||||
URL urlToUse = URL (url).withPOSTData (*postData);
|
||||
connectionThread.reset (new ConnectionThread (*this, urlToUse, *headers));
|
||||
}
|
||||
}
|
||||
|
||||
void stop() override
|
||||
{
|
||||
connectionThread = nullptr;
|
||||
|
||||
getEnv()->CallVoidMethod ((jobject) getView(), JuceWebView.stopLoading);
|
||||
}
|
||||
|
||||
void goBack() override
|
||||
{
|
||||
connectionThread = nullptr;
|
||||
|
||||
auto* env = getEnv();
|
||||
auto view = (jobject) getView();
|
||||
|
||||
if (env->CallBooleanMethod (view, JuceWebView.canGoBack))
|
||||
env->CallVoidMethod (view, JuceWebView.goBack);
|
||||
else
|
||||
owner.reloadLastURL();
|
||||
}
|
||||
|
||||
void goForward() override
|
||||
{
|
||||
connectionThread = nullptr;
|
||||
|
||||
getEnv()->CallVoidMethod ((jobject) getView(), JuceWebView.goForward);
|
||||
}
|
||||
|
||||
void refresh() override
|
||||
{
|
||||
connectionThread = nullptr;
|
||||
|
||||
getEnv()->CallVoidMethod ((jobject) getView(), JuceWebView.reload);
|
||||
}
|
||||
|
||||
void handleAsyncUpdate() override
|
||||
{
|
||||
jassert (connectionThread != nullptr);
|
||||
|
||||
if (connectionThread == nullptr)
|
||||
return;
|
||||
|
||||
auto& result = connectionThread->getResult();
|
||||
|
||||
if (result.statusCode >= 200 && result.statusCode < 300)
|
||||
{
|
||||
auto url = javaString (result.url);
|
||||
auto data = javaString (result.data);
|
||||
auto mimeType = javaString ("text/html");
|
||||
auto encoding = javaString ("utf-8");
|
||||
|
||||
getEnv()->CallVoidMethod ((jobject) getView(), JuceWebView.loadDataWithBaseURL,
|
||||
url.get(), data.get(), mimeType.get(),
|
||||
encoding.get(), 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
owner.pageLoadHadNetworkError (result.description);
|
||||
}
|
||||
}
|
||||
|
||||
bool handlePageAboutToLoad (const String& url)
|
||||
{
|
||||
if (MessageManager::getInstance()->isThisTheMessageThread())
|
||||
return owner.pageAboutToLoad (url);
|
||||
|
||||
WeakReference<Platform> weakRef (this);
|
||||
|
||||
if (weakRef == nullptr)
|
||||
return false;
|
||||
|
||||
responseReadyEvent.reset();
|
||||
|
||||
bool shouldLoad = false;
|
||||
|
||||
MessageManager::callAsync ([weakRef, url, &shouldLoad]
|
||||
{
|
||||
if (weakRef == nullptr)
|
||||
return;
|
||||
|
||||
shouldLoad = weakRef->owner.pageAboutToLoad (url);
|
||||
|
||||
weakRef->responseReadyEvent.signal();
|
||||
});
|
||||
|
||||
responseReadyEvent.wait (-1);
|
||||
|
||||
return shouldLoad;
|
||||
}
|
||||
|
||||
WebBrowserComponent& owner;
|
||||
|
||||
private:
|
||||
class ConnectionThread final : private Thread
|
||||
{
|
||||
public:
|
||||
struct Result
|
||||
{
|
||||
String url;
|
||||
int statusCode = 0;
|
||||
String description;
|
||||
String data;
|
||||
};
|
||||
|
||||
ConnectionThread (Platform& ownerToUse,
|
||||
URL& url,
|
||||
const StringArray& headers)
|
||||
: Thread (SystemStats::getJUCEVersion() + ": WebBrowserComponent::Platform::ConnectionThread"),
|
||||
owner (ownerToUse),
|
||||
webInputStream (new WebInputStream (url, true))
|
||||
{
|
||||
webInputStream->withExtraHeaders (headers.joinIntoString ("\n"));
|
||||
webInputStream->withConnectionTimeout (10000);
|
||||
|
||||
result.url = url.toString (true);
|
||||
|
||||
startThread();
|
||||
}
|
||||
|
||||
~ConnectionThread() override
|
||||
{
|
||||
webInputStream->cancel();
|
||||
signalThreadShouldExit();
|
||||
waitForThreadToExit (10000);
|
||||
|
||||
webInputStream = nullptr;
|
||||
}
|
||||
|
||||
void run() override
|
||||
{
|
||||
if (! webInputStream->connect (nullptr))
|
||||
{
|
||||
result.description = "Could not establish connection";
|
||||
owner.triggerAsyncUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
result.statusCode = webInputStream->getStatusCode();
|
||||
result.description = "Status code: " + String (result.statusCode);
|
||||
readFromInputStream();
|
||||
owner.triggerAsyncUpdate();
|
||||
}
|
||||
|
||||
const Result& getResult() { return result; }
|
||||
|
||||
private:
|
||||
void readFromInputStream()
|
||||
{
|
||||
MemoryOutputStream ostream;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
if (threadShouldExit())
|
||||
return;
|
||||
|
||||
char buffer [8192];
|
||||
auto num = webInputStream->read (buffer, sizeof (buffer));
|
||||
|
||||
if (num <= 0)
|
||||
break;
|
||||
|
||||
ostream.write (buffer, (size_t) num);
|
||||
}
|
||||
|
||||
result.data = ostream.toUTF8();
|
||||
}
|
||||
|
||||
Platform& owner;
|
||||
std::unique_ptr<WebInputStream> webInputStream;
|
||||
Result result;
|
||||
};
|
||||
|
||||
class MessageThreadResourceRequester : private AsyncUpdater
|
||||
{
|
||||
public:
|
||||
explicit MessageThreadResourceRequester (Platform& ownerIn)
|
||||
: owner (ownerIn)
|
||||
{}
|
||||
|
||||
~MessageThreadResourceRequester() override
|
||||
{
|
||||
cancelPendingUpdate();
|
||||
}
|
||||
|
||||
// The Platform destructor will block for as long as a Java thread waits for a Resource, so this
|
||||
// must be called from ~Platform().
|
||||
void cancelAllResourceRequests()
|
||||
{
|
||||
std::lock_guard lock { queueMutex };
|
||||
|
||||
while (! queue.empty())
|
||||
{
|
||||
auto* r = queue.front();
|
||||
queue.pop_front();
|
||||
r->event.signal();
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<Resource> handleResourceRequestOnMessageThread (StringRef uri)
|
||||
{
|
||||
if (MessageManager::getInstance()->isThisTheMessageThread())
|
||||
return owner.owner.impl->handleResourceRequest (uri);
|
||||
|
||||
WaitableResource resource { uri };
|
||||
|
||||
{
|
||||
std::lock_guard lock { queueMutex };
|
||||
queue.push_back (&resource);
|
||||
}
|
||||
|
||||
triggerAsyncUpdate();
|
||||
resource.event.wait (-1);
|
||||
|
||||
return resource.resource;
|
||||
}
|
||||
|
||||
private:
|
||||
struct WaitableResource
|
||||
{
|
||||
explicit WaitableResource (StringRef uriIn) : uri (uriIn) {}
|
||||
|
||||
String uri;
|
||||
WaitableEvent event;
|
||||
std::optional<WebBrowserComponent::Resource> resource;
|
||||
};
|
||||
|
||||
void handleAsyncUpdate() override
|
||||
{
|
||||
const auto popFront = [&]() -> WaitableResource*
|
||||
{
|
||||
const std::lock_guard lock { queueMutex };
|
||||
|
||||
if (queue.empty())
|
||||
return nullptr;
|
||||
|
||||
const ScopeGuard scope { [&] { queue.pop_front(); } };
|
||||
|
||||
return queue.front();
|
||||
};
|
||||
|
||||
for (auto* r = popFront(); r != nullptr; r = popFront())
|
||||
{
|
||||
r->resource = (owner.owner.impl->handleResourceRequest (r->uri));
|
||||
r->event.signal();
|
||||
}
|
||||
}
|
||||
|
||||
Platform& owner;
|
||||
std::deque<WaitableResource*> queue;
|
||||
std::mutex queueMutex;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
|
||||
METHOD (constructor, "<init>", "(J)V") \
|
||||
METHOD (hostDeleted, "hostDeleted", "()V") \
|
||||
CALLBACK (generatedCallback<&Platform::webViewReceivedHttpError>, "webViewReceivedHttpError", "(JLandroid/webkit/WebView;Landroid/webkit/WebResourceRequest;Landroid/webkit/WebResourceResponse;)V") \
|
||||
CALLBACK (generatedCallback<&Platform::handleResourceRequest>, "handleResourceRequest", "(JLandroid/webkit/WebView;Ljava/lang/String;)Landroid/webkit/WebResourceResponse;") \
|
||||
CALLBACK (generatedCallback<&Platform::webViewPageLoadFinished>, "webViewPageLoadFinished", "(JLandroid/webkit/WebView;Ljava/lang/String;)V") \
|
||||
CALLBACK (generatedCallback<&Platform::webViewReceivedSslError>, "webViewReceivedSslError", "(JLandroid/webkit/WebView;Landroid/webkit/SslErrorHandler;Landroid/net/http/SslError;)V") \
|
||||
CALLBACK (generatedCallback<&Platform::webViewCloseWindowRequest>, "webViewCloseWindowRequest", "(JLandroid/webkit/WebView;)V") \
|
||||
CALLBACK (generatedCallback<&Platform::webViewCreateWindowRequest>, "webViewCreateWindowRequest", "(JLandroid/webkit/WebView;)V") \
|
||||
CALLBACK (generatedCallback<&Platform::evaluationResultHandler>, "evaluationResultHandler", "(JJLjava/lang/String;)V") \
|
||||
CALLBACK (generatedCallback<&Platform::postMessageHandler>, "postMessage", "(JLjava/lang/String;)Ljava/lang/String;") \
|
||||
CALLBACK (generatedCallback<&Platform::pageAboutToLoad>, "pageAboutToLoad", "(JLjava/lang/String;)Z")
|
||||
|
||||
DECLARE_JNI_CLASS_WITH_BYTECODE (NativeInterface, "com/rmsl/juce/JuceWebViewClasses$NativeInterface", 16, juceWebViewClasses)
|
||||
#undef JNI_CLASS_MEMBERS
|
||||
|
||||
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
|
||||
METHOD (create, "<init>", "(Ljava/lang/String;Ljava/lang/String;Ljava/io/InputStream;)V") \
|
||||
METHOD (setResponseHeaders, "setResponseHeaders", "(Ljava/util/Map;)V") \
|
||||
|
||||
DECLARE_JNI_CLASS (WebResourceResponse, "android/webkit/WebResourceResponse")
|
||||
#undef JNI_CLASS_MEMBERS
|
||||
|
||||
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
|
||||
METHOD (create, "<init>", "([B)V") \
|
||||
|
||||
DECLARE_JNI_CLASS (ByteArrayInputStream, "java/io/ByteArrayInputStream")
|
||||
#undef JNI_CLASS_MEMBERS
|
||||
|
||||
static jobject handleResourceRequest (JNIEnv* env, Platform& t, jobject /*webView*/, jstring url)
|
||||
{
|
||||
const auto uriString = juceString (url);
|
||||
|
||||
if (const auto resourceRequestUri = uriString.fromFirstOccurrenceOf ("https://juce.backend", false, false);
|
||||
resourceRequestUri.isNotEmpty())
|
||||
{
|
||||
if (auto response = t.resourceRequester.handleResourceRequestOnMessageThread (resourceRequestUri))
|
||||
{
|
||||
jbyteArray byteArray = env->NewByteArray ((jsize) response->data.size());
|
||||
env->SetByteArrayRegion (byteArray, 0, (jsize) response->data.size(), (jbyte*) response->data.data());
|
||||
LocalRef<jobject> byteArrayInputStream { env->NewObject (ByteArrayInputStream, ByteArrayInputStream.create, byteArray) };
|
||||
auto resource = env->NewObject (WebResourceResponse,
|
||||
WebResourceResponse.create,
|
||||
javaString (response->mimeType).get(),
|
||||
response->mimeType.contains ("text") ? javaString ("utf-8").get() : nullptr,
|
||||
byteArrayInputStream.get());
|
||||
|
||||
if (const auto allowedOrigin = t.owner.impl->options.getAllowedOrigin())
|
||||
{
|
||||
LocalRef<jobject> headersMap { env->NewObject (JavaHashMap,
|
||||
JavaHashMap.constructorWithCapacity,
|
||||
1) };
|
||||
|
||||
env->CallObjectMethod (headersMap, JavaMap.put,
|
||||
javaString ("Access-Control-Allow-Origin").get(),
|
||||
javaString (*allowedOrigin).get());
|
||||
|
||||
env->CallVoidMethod (resource,
|
||||
WebResourceResponse.setResponseHeaders,
|
||||
headersMap.get());
|
||||
}
|
||||
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void webViewPageLoadFinished (JNIEnv*, Platform& t, jobject /*webView*/, jstring url)
|
||||
{
|
||||
t.owner.pageFinishedLoading (juceString (url));
|
||||
}
|
||||
|
||||
static void webViewReceivedSslError (JNIEnv* env, Platform& t, jobject /*webView*/, jobject /*sslErrorHandler*/, jobject sslError)
|
||||
{
|
||||
const auto errorString = LocalRef<jstring> ((jstring) env->CallObjectMethod (sslError, SslError.toString));
|
||||
t.owner.pageLoadHadNetworkError (juceString (errorString));
|
||||
}
|
||||
|
||||
static void evaluationResultHandler (JNIEnv*, Platform& t, jlong evalId, jstring result)
|
||||
{
|
||||
auto it = t.evaluationCallbacks.find ((int64) evalId);
|
||||
|
||||
if (it == t.evaluationCallbacks.end())
|
||||
return;
|
||||
|
||||
(it->second) (EvaluationResult { JSON::fromString (juceString (result)) });
|
||||
|
||||
t.evaluationCallbacks.erase (it);
|
||||
}
|
||||
|
||||
static jobject postMessageHandler (JNIEnv*, Platform& t, jstring message)
|
||||
{
|
||||
t.messageThreadExecutor.execute ([&t, msg = juceString (message)]
|
||||
{
|
||||
var obj;
|
||||
|
||||
if (! JSON::parse (msg, obj).wasOk())
|
||||
{
|
||||
jassertfalse;
|
||||
return;
|
||||
}
|
||||
|
||||
t.owner.impl->handleNativeEvent (obj);
|
||||
});
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static bool pageAboutToLoad (JNIEnv*, Platform& t, jstring url)
|
||||
{
|
||||
return t.handlePageAboutToLoad (juceString (url));
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static void webViewCloseWindowRequest (JNIEnv*, Platform& t, jobject)
|
||||
{
|
||||
t.owner.windowCloseRequest();
|
||||
}
|
||||
|
||||
static void webViewCreateWindowRequest (JNIEnv*, Platform& t, jobject)
|
||||
{
|
||||
t.owner.newWindowAttemptingToLoad ({});
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static void webViewReceivedHttpError (JNIEnv* env, Platform& t, jobject /*webView*/, jobject /*request*/, jobject errorResponse)
|
||||
{
|
||||
LocalRef<jclass> responseClass (env->FindClass ("android/webkit/WebResourceResponse"));
|
||||
|
||||
if (responseClass != nullptr)
|
||||
{
|
||||
jmethodID method = env->GetMethodID (responseClass, "getReasonPhrase", "()Ljava/lang/String;");
|
||||
|
||||
if (method != nullptr)
|
||||
{
|
||||
auto errorString = LocalRef<jstring> ((jstring) env->CallObjectMethod (errorResponse, method));
|
||||
|
||||
t.owner.pageLoadHadNetworkError (juceString (errorString));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Should never get here!
|
||||
jassertfalse;
|
||||
t.owner.pageLoadHadNetworkError ({});
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
std::unique_ptr<ConnectionThread> connectionThread;
|
||||
WaitableEvent responseReadyEvent;
|
||||
std::unordered_map<int64, EvaluationCallback> evaluationCallbacks;
|
||||
MessageThreadExecutor messageThreadExecutor;
|
||||
MessageThreadResourceRequester resourceRequester { *this };
|
||||
|
||||
WeakReference<Platform>::Master masterReference;
|
||||
friend class WeakReference<Platform>;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
auto WebBrowserComponent::Impl::createAndInitPlatformDependentPart (WebBrowserComponent::Impl& impl,
|
||||
const WebBrowserComponent::Options& options,
|
||||
const StringArray& userScripts)
|
||||
-> std::unique_ptr<PlatformInterface>
|
||||
{
|
||||
return std::make_unique<Platform> (impl.owner, options, userScripts);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void WebBrowserComponent::clearCookies()
|
||||
{
|
||||
auto* env = getEnv();
|
||||
|
||||
auto cookieManager = LocalRef<jobject> (env->CallStaticObjectMethod (AndroidCookieManager,
|
||||
AndroidCookieManager.getInstance));
|
||||
|
||||
jmethodID clearCookiesMethod = env->GetMethodID (AndroidCookieManager, "removeAllCookies", "(Landroid/webkit/ValueCallback;)V");
|
||||
env->CallVoidMethod (cookieManager, clearCookiesMethod, 0);
|
||||
}
|
||||
|
||||
bool WebBrowserComponent::areOptionsSupported (const Options& options)
|
||||
{
|
||||
return (options.getBackend() == Options::Backend::defaultBackend);
|
||||
}
|
||||
|
||||
} // namespace juce
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1316
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,739 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE framework.
|
||||
Copyright (c) Raw Material Software Limited
|
||||
|
||||
JUCE is an open source framework subject to commercial or open source
|
||||
licensing.
|
||||
|
||||
By downloading, installing, or using the JUCE framework, or combining the
|
||||
JUCE framework with any other source code, object code, content or any other
|
||||
copyrightable work, you agree to the terms of the JUCE End User Licence
|
||||
Agreement, and all incorporated terms including the JUCE Privacy Policy and
|
||||
the JUCE Website Terms of Service, as applicable, which will bind you. If you
|
||||
do not agree to the terms of these agreements, we will not license the JUCE
|
||||
framework to you, and you must discontinue the installation or download
|
||||
process and cease use of the JUCE framework.
|
||||
|
||||
JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
|
||||
JUCE Privacy Policy: https://juce.com/juce-privacy-policy
|
||||
JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/
|
||||
|
||||
Or:
|
||||
|
||||
You may also use this code under the terms of the AGPLv3:
|
||||
https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
|
||||
THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
|
||||
WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
|
||||
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
::Window juce_createKeyProxyWindow (ComponentPeer*);
|
||||
void juce_deleteKeyProxyWindow (::Window);
|
||||
|
||||
//==============================================================================
|
||||
enum
|
||||
{
|
||||
maxXEmbedVersionToSupport = 0
|
||||
};
|
||||
|
||||
enum
|
||||
{
|
||||
XEMBED_MAPPED = (1<<0)
|
||||
};
|
||||
|
||||
enum
|
||||
{
|
||||
XEMBED_EMBEDDED_NOTIFY = 0,
|
||||
XEMBED_WINDOW_ACTIVATE = 1,
|
||||
XEMBED_WINDOW_DEACTIVATE = 2,
|
||||
XEMBED_REQUEST_FOCUS = 3,
|
||||
XEMBED_FOCUS_IN = 4,
|
||||
XEMBED_FOCUS_OUT = 5,
|
||||
XEMBED_FOCUS_NEXT = 6,
|
||||
XEMBED_FOCUS_PREV = 7,
|
||||
XEMBED_MODALITY_ON = 10,
|
||||
XEMBED_MODALITY_OFF = 11,
|
||||
XEMBED_REGISTER_ACCELERATOR = 12,
|
||||
XEMBED_UNREGISTER_ACCELERATOR = 13,
|
||||
XEMBED_ACTIVATE_ACCELERATOR = 14
|
||||
};
|
||||
|
||||
enum
|
||||
{
|
||||
XEMBED_FOCUS_CURRENT = 0,
|
||||
XEMBED_FOCUS_FIRST = 1,
|
||||
XEMBED_FOCUS_LAST = 2
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class XEmbedComponent::Pimpl : private ComponentListener
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
struct SharedKeyWindow final : public ReferenceCountedObject
|
||||
{
|
||||
SharedKeyWindow (ComponentPeer* peerToUse)
|
||||
: keyPeer (peerToUse),
|
||||
keyProxy (juce_createKeyProxyWindow (keyPeer)),
|
||||
association (peerToUse, keyProxy)
|
||||
{}
|
||||
|
||||
~SharedKeyWindow()
|
||||
{
|
||||
association = {};
|
||||
juce_deleteKeyProxyWindow (keyProxy);
|
||||
|
||||
auto& keyWindows = getKeyWindows();
|
||||
keyWindows.remove (keyPeer);
|
||||
}
|
||||
|
||||
using Ptr = ReferenceCountedObjectPtr<SharedKeyWindow>;
|
||||
|
||||
//==============================================================================
|
||||
Window getHandle() { return keyProxy; }
|
||||
|
||||
static Window getCurrentFocusWindow (ComponentPeer* peerToLookFor)
|
||||
{
|
||||
auto& keyWindows = getKeyWindows();
|
||||
|
||||
if (peerToLookFor != nullptr)
|
||||
if (auto* foundKeyWindow = keyWindows[peerToLookFor])
|
||||
return foundKeyWindow->keyProxy;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static SharedKeyWindow::Ptr getKeyWindowForPeer (ComponentPeer* peerToLookFor)
|
||||
{
|
||||
jassert (peerToLookFor != nullptr);
|
||||
|
||||
auto& keyWindows = getKeyWindows();
|
||||
auto foundKeyWindow = keyWindows[peerToLookFor];
|
||||
|
||||
if (foundKeyWindow == nullptr)
|
||||
{
|
||||
foundKeyWindow = new SharedKeyWindow (peerToLookFor);
|
||||
keyWindows.set (peerToLookFor, foundKeyWindow);
|
||||
}
|
||||
|
||||
return foundKeyWindow;
|
||||
}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
ComponentPeer* keyPeer;
|
||||
Window keyProxy;
|
||||
ScopedWindowAssociation association;
|
||||
|
||||
static HashMap<ComponentPeer*, SharedKeyWindow*>& getKeyWindows()
|
||||
{
|
||||
// store a weak reference to the shared key windows
|
||||
static HashMap<ComponentPeer*, SharedKeyWindow*> keyWindows;
|
||||
return keyWindows;
|
||||
}
|
||||
};
|
||||
|
||||
public:
|
||||
//==============================================================================
|
||||
Pimpl (XEmbedComponent& parent, Window x11Window,
|
||||
bool wantsKeyboardFocus, bool isClientInitiated, bool shouldAllowResize)
|
||||
: owner (parent),
|
||||
infoAtom (XWindowSystem::getInstance()->getAtoms().XembedInfo),
|
||||
messageTypeAtom (XWindowSystem::getInstance()->getAtoms().XembedMsgType),
|
||||
clientInitiated (isClientInitiated),
|
||||
wantsFocus (wantsKeyboardFocus),
|
||||
allowResize (shouldAllowResize)
|
||||
{
|
||||
getWidgets().add (this);
|
||||
|
||||
createHostWindow();
|
||||
|
||||
if (clientInitiated)
|
||||
setClient (x11Window, true);
|
||||
|
||||
owner.setWantsKeyboardFocus (wantsFocus);
|
||||
owner.addComponentListener (this);
|
||||
}
|
||||
|
||||
~Pimpl() override
|
||||
{
|
||||
owner.removeComponentListener (this);
|
||||
setClient (0, true);
|
||||
|
||||
if (host != 0)
|
||||
{
|
||||
auto dpy = getDisplay();
|
||||
|
||||
X11Symbols::getInstance()->xDestroyWindow (dpy, host);
|
||||
X11Symbols::getInstance()->xSync (dpy, false);
|
||||
|
||||
auto mask = NoEventMask | KeyPressMask | KeyReleaseMask
|
||||
| EnterWindowMask | LeaveWindowMask | PointerMotionMask
|
||||
| KeymapStateMask | ExposureMask | StructureNotifyMask
|
||||
| FocusChangeMask;
|
||||
|
||||
XEvent event;
|
||||
while (X11Symbols::getInstance()->xCheckWindowEvent (dpy, host, mask, &event) == True)
|
||||
{}
|
||||
|
||||
host = 0;
|
||||
}
|
||||
|
||||
getWidgets().removeAllInstancesOf (this);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void setClient (Window xembedClient, bool shouldReparent)
|
||||
{
|
||||
removeClient();
|
||||
|
||||
if (xembedClient != 0)
|
||||
{
|
||||
auto dpy = getDisplay();
|
||||
|
||||
client = xembedClient;
|
||||
|
||||
// if the client has initiated the component then keep the clients size
|
||||
// otherwise the client should use the host's window' size
|
||||
if (clientInitiated)
|
||||
{
|
||||
configureNotify();
|
||||
}
|
||||
else
|
||||
{
|
||||
auto newBounds = getX11BoundsFromJuce();
|
||||
X11Symbols::getInstance()->xResizeWindow (dpy, client, static_cast<unsigned int> (newBounds.getWidth()),
|
||||
static_cast<unsigned int> (newBounds.getHeight()));
|
||||
}
|
||||
|
||||
auto eventMask = StructureNotifyMask | PropertyChangeMask | FocusChangeMask;
|
||||
|
||||
XWindowAttributes clientAttr;
|
||||
X11Symbols::getInstance()->xGetWindowAttributes (dpy, client, &clientAttr);
|
||||
|
||||
if ((eventMask & clientAttr.your_event_mask) != eventMask)
|
||||
X11Symbols::getInstance()->xSelectInput (dpy, client, clientAttr.your_event_mask | eventMask);
|
||||
|
||||
getXEmbedMappedFlag();
|
||||
|
||||
if (shouldReparent)
|
||||
X11Symbols::getInstance()->xReparentWindow (dpy, client, host, 0, 0);
|
||||
|
||||
if (supportsXembed)
|
||||
sendXEmbedEvent (CurrentTime, XEMBED_EMBEDDED_NOTIFY, 0, (long) host, xembedVersion);
|
||||
|
||||
updateMapping();
|
||||
}
|
||||
}
|
||||
|
||||
void focusGained (FocusChangeType changeType, FocusChangeDirection direction)
|
||||
{
|
||||
if (client != 0 && supportsXembed && wantsFocus)
|
||||
{
|
||||
updateKeyFocus();
|
||||
|
||||
const auto xembedDirection = [&]
|
||||
{
|
||||
if (direction == FocusChangeDirection::forward)
|
||||
return XEMBED_FOCUS_FIRST;
|
||||
|
||||
if (direction == FocusChangeDirection::backward)
|
||||
return XEMBED_FOCUS_LAST;
|
||||
|
||||
return XEMBED_FOCUS_CURRENT;
|
||||
}();
|
||||
|
||||
sendXEmbedEvent (CurrentTime, XEMBED_FOCUS_IN,
|
||||
(changeType == focusChangedByTabKey ? xembedDirection : XEMBED_FOCUS_CURRENT));
|
||||
}
|
||||
}
|
||||
|
||||
void focusLost (FocusChangeType)
|
||||
{
|
||||
if (client != 0 && supportsXembed && wantsFocus)
|
||||
{
|
||||
sendXEmbedEvent (CurrentTime, XEMBED_FOCUS_OUT);
|
||||
updateKeyFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void broughtToFront()
|
||||
{
|
||||
if (client != 0 && supportsXembed)
|
||||
sendXEmbedEvent (CurrentTime, XEMBED_WINDOW_ACTIVATE);
|
||||
}
|
||||
|
||||
unsigned long getHostWindowID()
|
||||
{
|
||||
// You are using the client initiated version of the protocol. You cannot
|
||||
// retrieve the window id of the host. Please read the documentation for
|
||||
// the XEmebedComponent class.
|
||||
jassert (! clientInitiated);
|
||||
|
||||
return host;
|
||||
}
|
||||
|
||||
void updateEmbeddedBounds()
|
||||
{
|
||||
componentMovedOrResized (owner, true, true);
|
||||
}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
XEmbedComponent& owner;
|
||||
Window client = 0, host = 0;
|
||||
Atom infoAtom, messageTypeAtom;
|
||||
|
||||
bool clientInitiated;
|
||||
bool wantsFocus = false;
|
||||
bool allowResize = false;
|
||||
bool supportsXembed = false;
|
||||
bool hasBeenMapped = false;
|
||||
int xembedVersion = maxXEmbedVersionToSupport;
|
||||
|
||||
ComponentPeer* lastPeer = nullptr;
|
||||
SharedKeyWindow::Ptr keyWindow;
|
||||
|
||||
//==============================================================================
|
||||
void componentParentHierarchyChanged (Component&) override { peerChanged (owner.getPeer()); }
|
||||
void componentMovedOrResized (Component&, bool, bool) override
|
||||
{
|
||||
if (host != 0 && lastPeer != nullptr)
|
||||
{
|
||||
auto dpy = getDisplay();
|
||||
auto newBounds = getX11BoundsFromJuce();
|
||||
XWindowAttributes attr;
|
||||
|
||||
if (X11Symbols::getInstance()->xGetWindowAttributes (dpy, host, &attr))
|
||||
{
|
||||
Rectangle<int> currentBounds (attr.x, attr.y, attr.width, attr.height);
|
||||
if (currentBounds != newBounds)
|
||||
{
|
||||
X11Symbols::getInstance()->xMoveResizeWindow (dpy, host, newBounds.getX(), newBounds.getY(),
|
||||
static_cast<unsigned int> (newBounds.getWidth()),
|
||||
static_cast<unsigned int> (newBounds.getHeight()));
|
||||
}
|
||||
}
|
||||
|
||||
if (client != 0 && X11Symbols::getInstance()->xGetWindowAttributes (dpy, client, &attr))
|
||||
{
|
||||
Rectangle<int> currentBounds (attr.x, attr.y, attr.width, attr.height);
|
||||
|
||||
if ((currentBounds.getWidth() != newBounds.getWidth()
|
||||
|| currentBounds.getHeight() != newBounds.getHeight()))
|
||||
{
|
||||
X11Symbols::getInstance()->xMoveResizeWindow (dpy, client, 0, 0,
|
||||
static_cast<unsigned int> (newBounds.getWidth()),
|
||||
static_cast<unsigned int> (newBounds.getHeight()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void createHostWindow()
|
||||
{
|
||||
auto dpy = getDisplay();
|
||||
int defaultScreen = X11Symbols::getInstance()->xDefaultScreen (dpy);
|
||||
Window root = X11Symbols::getInstance()->xRootWindow (dpy, defaultScreen);
|
||||
|
||||
XSetWindowAttributes swa;
|
||||
swa.border_pixel = 0;
|
||||
swa.background_pixmap = None;
|
||||
swa.override_redirect = True;
|
||||
swa.event_mask = SubstructureNotifyMask | StructureNotifyMask | FocusChangeMask;
|
||||
|
||||
host = X11Symbols::getInstance()->xCreateWindow (dpy, root, 0, 0, 1, 1, 0, CopyFromParent,
|
||||
InputOutput, CopyFromParent,
|
||||
CWEventMask | CWBorderPixel | CWBackPixmap | CWOverrideRedirect,
|
||||
&swa);
|
||||
}
|
||||
|
||||
void removeClient()
|
||||
{
|
||||
if (client != 0)
|
||||
{
|
||||
auto dpy = getDisplay();
|
||||
X11Symbols::getInstance()->xSelectInput (dpy, client, 0);
|
||||
|
||||
keyWindow = nullptr;
|
||||
|
||||
int defaultScreen = X11Symbols::getInstance()->xDefaultScreen (dpy);
|
||||
Window root = X11Symbols::getInstance()->xRootWindow (dpy, defaultScreen);
|
||||
|
||||
if (hasBeenMapped)
|
||||
{
|
||||
X11Symbols::getInstance()->xUnmapWindow (dpy, client);
|
||||
hasBeenMapped = false;
|
||||
}
|
||||
|
||||
X11Symbols::getInstance()->xReparentWindow (dpy, client, root, 0, 0);
|
||||
client = 0;
|
||||
|
||||
X11Symbols::getInstance()->xSync (dpy, False);
|
||||
}
|
||||
}
|
||||
|
||||
void updateMapping()
|
||||
{
|
||||
if (client != 0)
|
||||
{
|
||||
const bool shouldBeMapped = getXEmbedMappedFlag();
|
||||
|
||||
if (shouldBeMapped != hasBeenMapped)
|
||||
{
|
||||
hasBeenMapped = shouldBeMapped;
|
||||
|
||||
if (shouldBeMapped)
|
||||
X11Symbols::getInstance()->xMapWindow (getDisplay(), client);
|
||||
else
|
||||
X11Symbols::getInstance()->xUnmapWindow (getDisplay(), client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Window getParentX11Window()
|
||||
{
|
||||
if (auto* peer = owner.getPeer())
|
||||
return reinterpret_cast<Window> (peer->getNativeHandle());
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
Display* getDisplay() { return XWindowSystem::getInstance()->getDisplay(); }
|
||||
|
||||
//==============================================================================
|
||||
bool getXEmbedMappedFlag()
|
||||
{
|
||||
XWindowSystemUtilities::GetXProperty embedInfo (getDisplay(), client, infoAtom, 0, 2, false, infoAtom);
|
||||
|
||||
if (embedInfo.success && embedInfo.actualFormat == 32
|
||||
&& embedInfo.numItems >= 2 && embedInfo.data != nullptr)
|
||||
{
|
||||
long version;
|
||||
memcpy (&version, embedInfo.data, sizeof (long));
|
||||
|
||||
supportsXembed = true;
|
||||
xembedVersion = jmin ((int) maxXEmbedVersionToSupport, (int) version);
|
||||
|
||||
long flags;
|
||||
memcpy (&flags, embedInfo.data + sizeof (long), sizeof (long));
|
||||
|
||||
return ((flags & XEMBED_MAPPED) != 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
supportsXembed = false;
|
||||
xembedVersion = maxXEmbedVersionToSupport;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void propertyChanged (const Atom& a)
|
||||
{
|
||||
if (a == infoAtom)
|
||||
updateMapping();
|
||||
}
|
||||
|
||||
void configureNotify()
|
||||
{
|
||||
XWindowAttributes attr;
|
||||
auto dpy = getDisplay();
|
||||
|
||||
if (X11Symbols::getInstance()->xGetWindowAttributes (dpy, client, &attr))
|
||||
{
|
||||
XWindowAttributes hostAttr;
|
||||
|
||||
if (X11Symbols::getInstance()->xGetWindowAttributes (dpy, host, &hostAttr))
|
||||
if (attr.width != hostAttr.width || attr.height != hostAttr.height)
|
||||
X11Symbols::getInstance()->xResizeWindow (dpy, host, (unsigned int) attr.width, (unsigned int) attr.height);
|
||||
|
||||
// as the client window is not on any screen yet, we need to guess
|
||||
// on which screen it might appear to get a scaling factor :-(
|
||||
auto& displays = Desktop::getInstance().getDisplays();
|
||||
auto* peer = owner.getPeer();
|
||||
const double scale = (peer != nullptr ? peer->getPlatformScaleFactor()
|
||||
: displays.getPrimaryDisplay()->scale);
|
||||
|
||||
Point<int> topLeftInPeer
|
||||
= (peer != nullptr ? peer->getComponent().getLocalPoint (&owner, Point<int> (0, 0))
|
||||
: owner.getBounds().getTopLeft());
|
||||
|
||||
Rectangle<int> newBounds (topLeftInPeer.getX(), topLeftInPeer.getY(),
|
||||
static_cast<int> (static_cast<double> (attr.width) / scale),
|
||||
static_cast<int> (static_cast<double> (attr.height) / scale));
|
||||
|
||||
|
||||
if (peer != nullptr)
|
||||
newBounds = owner.getLocalArea (&peer->getComponent(), newBounds);
|
||||
|
||||
jassert (newBounds.getX() == 0 && newBounds.getY() == 0);
|
||||
|
||||
if (newBounds != owner.getLocalBounds())
|
||||
owner.setSize (newBounds.getWidth(), newBounds.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
void peerChanged (ComponentPeer* newPeer)
|
||||
{
|
||||
if (newPeer != lastPeer)
|
||||
{
|
||||
if (lastPeer != nullptr)
|
||||
keyWindow = nullptr;
|
||||
|
||||
auto dpy = getDisplay();
|
||||
Window rootWindow = X11Symbols::getInstance()->xRootWindow (dpy, DefaultScreen (dpy));
|
||||
Rectangle<int> newBounds = getX11BoundsFromJuce();
|
||||
|
||||
if (newPeer == nullptr)
|
||||
X11Symbols::getInstance()->xUnmapWindow (dpy, host);
|
||||
|
||||
Window newParent = (newPeer != nullptr ? getParentX11Window() : rootWindow);
|
||||
X11Symbols::getInstance()->xReparentWindow (dpy, host, newParent, newBounds.getX(), newBounds.getY());
|
||||
|
||||
lastPeer = newPeer;
|
||||
|
||||
if (newPeer != nullptr)
|
||||
{
|
||||
if (wantsFocus)
|
||||
{
|
||||
keyWindow = SharedKeyWindow::getKeyWindowForPeer (newPeer);
|
||||
updateKeyFocus();
|
||||
}
|
||||
|
||||
componentMovedOrResized (owner, true, true);
|
||||
X11Symbols::getInstance()->xMapWindow (dpy, host);
|
||||
|
||||
broughtToFront();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updateKeyFocus()
|
||||
{
|
||||
if (lastPeer != nullptr && lastPeer->isFocused())
|
||||
X11Symbols::getInstance()->xSetInputFocus (getDisplay(), getCurrentFocusWindow (lastPeer), RevertToParent, CurrentTime);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void handleXembedCmd (const ::Time& /*xTime*/, long opcode, long /*detail*/, long /*data1*/, long /*data2*/)
|
||||
{
|
||||
if (auto* peer = owner.getPeer())
|
||||
peer->getCurrentModifiersRealtime();
|
||||
|
||||
switch (opcode)
|
||||
{
|
||||
case XEMBED_REQUEST_FOCUS:
|
||||
if (wantsFocus)
|
||||
owner.grabKeyboardFocus();
|
||||
break;
|
||||
|
||||
case XEMBED_FOCUS_NEXT:
|
||||
if (wantsFocus)
|
||||
owner.moveKeyboardFocusToSibling (true);
|
||||
break;
|
||||
|
||||
case XEMBED_FOCUS_PREV:
|
||||
if (wantsFocus)
|
||||
owner.moveKeyboardFocusToSibling (false);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool handleX11Event (const XEvent& e)
|
||||
{
|
||||
if (e.xany.window == client && client != 0)
|
||||
{
|
||||
switch (e.type)
|
||||
{
|
||||
case PropertyNotify:
|
||||
propertyChanged (e.xproperty.atom);
|
||||
return true;
|
||||
|
||||
case ConfigureNotify:
|
||||
if (allowResize)
|
||||
configureNotify();
|
||||
else
|
||||
MessageManager::callAsync ([this] {componentMovedOrResized (owner, true, true);});
|
||||
|
||||
return true;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (e.xany.window == host && host != 0)
|
||||
{
|
||||
switch (e.type)
|
||||
{
|
||||
case ReparentNotify:
|
||||
if (e.xreparent.parent == host && e.xreparent.window != client)
|
||||
{
|
||||
setClient (e.xreparent.window, false);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
case CreateNotify:
|
||||
if (e.xcreatewindow.parent != e.xcreatewindow.window && e.xcreatewindow.parent == host && e.xcreatewindow.window != client)
|
||||
{
|
||||
setClient (e.xcreatewindow.window, false);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
case GravityNotify:
|
||||
componentMovedOrResized (owner, true, true);
|
||||
return true;
|
||||
|
||||
case ClientMessage:
|
||||
if (e.xclient.message_type == messageTypeAtom && e.xclient.format == 32)
|
||||
{
|
||||
handleXembedCmd ((::Time) e.xclient.data.l[0], e.xclient.data.l[1],
|
||||
e.xclient.data.l[2], e.xclient.data.l[3],
|
||||
e.xclient.data.l[4]);
|
||||
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void sendXEmbedEvent (const ::Time& xTime, long opcode,
|
||||
long opcodeMinor = 0, long data1 = 0, long data2 = 0)
|
||||
{
|
||||
XClientMessageEvent msg;
|
||||
auto dpy = getDisplay();
|
||||
|
||||
::memset (&msg, 0, sizeof (XClientMessageEvent));
|
||||
msg.window = client;
|
||||
msg.type = ClientMessage;
|
||||
msg.message_type = messageTypeAtom;
|
||||
msg.format = 32;
|
||||
msg.data.l[0] = (long) xTime;
|
||||
msg.data.l[1] = opcode;
|
||||
msg.data.l[2] = opcodeMinor;
|
||||
msg.data.l[3] = data1;
|
||||
msg.data.l[4] = data2;
|
||||
|
||||
X11Symbols::getInstance()->xSendEvent (dpy, client, False, NoEventMask, (XEvent*) &msg);
|
||||
X11Symbols::getInstance()->xSync (dpy, False);
|
||||
}
|
||||
|
||||
Rectangle<int> getX11BoundsFromJuce()
|
||||
{
|
||||
if (auto* peer = owner.getPeer())
|
||||
{
|
||||
auto r = peer->getComponent().getLocalArea (&owner, owner.getLocalBounds());
|
||||
return r * peer->getPlatformScaleFactor() * peer->getComponent().getDesktopScaleFactor();
|
||||
}
|
||||
|
||||
return owner.getLocalBounds();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
friend bool juce::juce_handleXEmbedEvent (ComponentPeer*, void*);
|
||||
friend unsigned long juce::juce_getCurrentFocusWindow (ComponentPeer*);
|
||||
|
||||
static Array<Pimpl*>& getWidgets()
|
||||
{
|
||||
static Array<Pimpl*> i;
|
||||
return i;
|
||||
}
|
||||
|
||||
static bool dispatchX11Event (ComponentPeer* p, const XEvent* eventArg)
|
||||
{
|
||||
if (eventArg != nullptr)
|
||||
{
|
||||
auto& e = *eventArg;
|
||||
|
||||
if (auto w = e.xany.window)
|
||||
for (auto* widget : getWidgets())
|
||||
if (w == widget->host || w == widget->client)
|
||||
return widget->handleX11Event (e);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (auto* widget : getWidgets())
|
||||
if (widget->owner.getPeer() == p)
|
||||
widget->peerChanged (nullptr);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static Window getCurrentFocusWindow (ComponentPeer* p)
|
||||
{
|
||||
if (p != nullptr)
|
||||
{
|
||||
for (auto* widget : getWidgets())
|
||||
if (widget->owner.getPeer() == p && widget->owner.hasKeyboardFocus (false))
|
||||
return widget->client;
|
||||
}
|
||||
|
||||
return SharedKeyWindow::getCurrentFocusWindow (p);
|
||||
}
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
XEmbedComponent::XEmbedComponent (bool wantsKeyboardFocus, bool allowForeignWidgetToResizeComponent)
|
||||
: pimpl (new Pimpl (*this, 0, wantsKeyboardFocus, false, allowForeignWidgetToResizeComponent))
|
||||
{
|
||||
setOpaque (true);
|
||||
}
|
||||
|
||||
XEmbedComponent::XEmbedComponent (unsigned long wID, bool wantsKeyboardFocus, bool allowForeignWidgetToResizeComponent)
|
||||
: pimpl (new Pimpl (*this, wID, wantsKeyboardFocus, true, allowForeignWidgetToResizeComponent))
|
||||
{
|
||||
setOpaque (true);
|
||||
}
|
||||
|
||||
XEmbedComponent::~XEmbedComponent() {}
|
||||
|
||||
void XEmbedComponent::paint (Graphics& g)
|
||||
{
|
||||
g.fillAll (Colours::lightgrey);
|
||||
}
|
||||
|
||||
void XEmbedComponent::focusGainedWithDirection (FocusChangeType changeType, FocusChangeDirection direction)
|
||||
{
|
||||
pimpl->focusGained (changeType, direction);
|
||||
}
|
||||
|
||||
void XEmbedComponent::focusLost (FocusChangeType changeType) { pimpl->focusLost (changeType); }
|
||||
void XEmbedComponent::broughtToFront() { pimpl->broughtToFront(); }
|
||||
unsigned long XEmbedComponent::getHostWindowID() { return pimpl->getHostWindowID(); }
|
||||
void XEmbedComponent::removeClient() { pimpl->setClient (0, true); }
|
||||
void XEmbedComponent::updateEmbeddedBounds() { pimpl->updateEmbeddedBounds(); }
|
||||
|
||||
//==============================================================================
|
||||
bool juce_handleXEmbedEvent (ComponentPeer* p, void* e)
|
||||
{
|
||||
return XEmbedComponent::Pimpl::dispatchX11Event (p, reinterpret_cast<const XEvent*> (e));
|
||||
}
|
||||
|
||||
unsigned long juce_getCurrentFocusWindow (ComponentPeer* peer)
|
||||
{
|
||||
return (unsigned long) XEmbedComponent::Pimpl::getCurrentFocusWindow (peer);
|
||||
}
|
||||
|
||||
} // namespace juce
|
||||
Reference in New Issue
Block a user