Browse Source

Added example of CEF integration in Jetpack Compose.

pull/4/head
spvessel 4 years ago
parent
commit
fb13f9b5b4
  1. 6
      cef/.gitattributes
  2. 5
      cef/.gitignore
  3. 16
      cef/README.md
  4. 30
      cef/build.gradle.kts
  5. 5
      cef/gradle/wrapper/gradle-wrapper.properties
  6. 185
      cef/gradlew
  7. 104
      cef/gradlew.bat
  8. 8
      cef/settings.gradle.kts
  9. 89
      cef/src/main/kotlin/org/jetbrains/compose/desktop/App.kt
  10. 89
      cef/src/main/kotlin/org/jetbrains/compose/desktop/browser/BrowserState.kt
  11. 106
      cef/src/main/kotlin/org/jetbrains/compose/desktop/browser/CefView.kt
  12. 152
      cef/src/main/kotlin/org/jetbrains/compose/desktop/browser/ComposeBrowserWrapper.kt
  13. 483
      cef/third_party/java-cef-jb-compose-patch/jb_compose_support.patch

6
cef/.gitattributes vendored

@ -0,0 +1,6 @@
#
# https://help.github.com/articles/dealing-with-line-endings/
#
# These are explicitly windows files and should use crlf
*.bat text eol=crlf

5
cef/.gitignore vendored

@ -0,0 +1,5 @@
# Ignore Gradle project-specific cache directory
.gradle
# Ignore Gradle build output directory
build

16
cef/README.md

@ -0,0 +1,16 @@
CEF integration for Desktop Jetpack Compose.
Setup:
1. Clone the java-cef repository (``git clone https://bitbucket.org/chromiumembedded/java-cef.git``) into ``/third_party`` directory.
2. Apply patch ``/third_party/java-cef-jb-compose-patch/jb_compose_support.patch`` to ``/third_party/java-cef``
3. Download [skiko-jvm-0.1.6.jar](https://github.com/JetBrains/skiko/releases) library and copy it to ``/third_party/java-cef/third_party/jogamp/jar`` directory.
4. Follow instructions to compile java-cef ([BranchesAndBuilding](https://bitbucket.org/chromiumembedded/java-cef/wiki/BranchesAndBuilding.md)) **until you reach step 3 of the instruction**.
5. Make **jcef.jar** - execute command in terminal (from ``/third_party/java-cef/tools`` directory):
Windows: ``make_jar.bat win64``
Linux: ``make_jar.sh linux64``
6. Copy **jcef.jar** file from ``/third_party/java-cef/out/win64`` (or ``/third_party/java-cef/out/linux64`` for Linux) to ``/libs`` directory.
Run example:
To run application execute in terminal: ``./gradlew run``
PS. Mac OS X is currently not supported.

30
cef/build.gradle.kts

@ -0,0 +1,30 @@
import org.jetbrains.compose.compose
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm") version "1.4.0"
id("org.jetbrains.compose") version "0.1.0-unmerged30"
application
}
repositories {
google()
jcenter()
maven("https://packages.jetbrains.team/maven/p/ui/dev")
}
dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
implementation(compose.desktop.all)
}
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
}
val libraryPath = "third_party/java-cef/jcef_build/native/Release"
application {
applicationDefaultJvmArgs = listOf("-Djava.library.path=$libraryPath")
mainClassName = "org.jetbrains.compose.desktop.AppKt"
}

5
cef/gradle/wrapper/gradle-wrapper.properties vendored

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
cef/gradlew vendored

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

104
cef/gradlew.bat vendored

@ -0,0 +1,104 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

8
cef/settings.gradle.kts

@ -0,0 +1,8 @@
pluginManagement {
repositories {
gradlePluginPortal()
maven("https://packages.jetbrains.team/maven/p/ui/dev")
}
}
rootProject.name = "example-cef-compose"

89
cef/src/main/kotlin/org/jetbrains/compose/desktop/App.kt

@ -0,0 +1,89 @@
package org.jetbrains.compose.desktop
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.MutableState
import androidx.compose.desktop.AppWindow
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.preferredHeight
import androidx.compose.foundation.layout.preferredSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Surface
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.TextField
import androidx.compose.material.Button
import androidx.compose.foundation.Text
import org.jetbrains.compose.desktop.browser.BrowserState
import org.jetbrains.compose.desktop.browser.CefView
fun main() {
AppWindow("CEF-compose", IntSize(800, 800)).show {
Surface(
modifier = Modifier.fillMaxSize(),
color = Color.DarkGray
) {
Column {
AddressBar()
Spacer(Modifier.height(10.dp))
WebView()
}
}
}
}
@Composable
private fun AddressBar() {
Surface(
color = Color.Transparent,
modifier = Modifier
.preferredHeight(58.dp)
.padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 0.dp)
) {
Row {
TextField(
backgroundColor = Color.White,
activeColor = Color.DarkGray,
inactiveColor = Color.DarkGray,
value = BrowserState.url.value,
onValueChange = {
BrowserState.url.value = it
},
modifier = Modifier.weight(1f),
shape = CircleShape,
label = { }
)
Spacer(Modifier.width(10.dp))
Button(
backgroundColor = Color(16, 180, 140),
modifier = Modifier.preferredHeight(48.dp),
shape = CircleShape,
onClick = { BrowserState.loadURL(BrowserState.url.value) }
) {
Text(text = "Go!")
}
}
}
}
@Composable
private fun WebView() {
Surface(
color = Color.Gray,
modifier = Modifier.fillMaxSize().padding(10.dp)
) {
if (BrowserState.isReady()) {
CefView()
}
}
}

89
cef/src/main/kotlin/org/jetbrains/compose/desktop/browser/BrowserState.kt

@ -0,0 +1,89 @@
package org.jetbrains.compose.desktop.browser
import androidx.compose.desktop.AppManager
import androidx.compose.desktop.AppFrame
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.MutableState
import androidx.compose.desktop.AppWindowAmbient
import org.jetbrains.skija.Bitmap
import org.jetbrains.skiko.HardwareLayer
import javax.swing.JFrame
import org.cef.CefApp
object BrowserState {
public val url = mutableStateOf("https://www.google.com")
private val isReady = mutableStateOf(false)
private val frames = mutableStateOf(0)
private lateinit var browser: ComposeBrowserWrapper
fun isReady(): Boolean {
return isReady.value
}
fun loadURL(url: String) {
if (!isReady.value) {
val app = AppManager.getCurrentFocusedWindow()
if (app != null) {
init(app, url)
}
} else {
isReady.value = false
browser.loadURL(url)
isReady.value = true
}
}
fun init(app: AppFrame, url: String) {
val window = app.window
if (!window.isVisible()) {
return
}
var layer = getHardwareLayer(window)
if (layer == null) {
throw Error("Browser initialization failed!")
}
browser = ComposeBrowserWrapper(
startURL = url,
layer = layer
)
isReady.value = true
}
fun getBitmap(): Bitmap {
frames.value++
return browser.getBitmap()
}
fun onLayout(x: Int, y: Int, width: Int, height: Int) {
browser.onLayout(x, y, width, height)
}
fun onActive() {
browser.onActive()
}
fun onDismiss() {
browser.onDismiss()
}
fun setFocused(value: Boolean) {
browser.setFocused(value)
}
fun onInvalidate(onInvalidate: (() -> Unit)?) {
browser.onInvalidate = onInvalidate
}
private fun getHardwareLayer(window: JFrame): HardwareLayer? {
val components = window.getContentPane().getComponents()
for (component in components) {
if (component is HardwareLayer) {
return component
}
}
return null
}
}

106
cef/src/main/kotlin/org/jetbrains/compose/desktop/browser/CefView.kt

@ -0,0 +1,106 @@
package org.jetbrains.compose.desktop.browser
import androidx.compose.runtime.Composable
import androidx.compose.runtime.emptyContent
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.onActive
import androidx.compose.runtime.onDispose
import androidx.compose.runtime.remember
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.layout
import androidx.compose.ui.layout.globalPosition
import androidx.compose.ui.onPositioned
import androidx.compose.material.Surface
import androidx.compose.ui.Modifier
import org.jetbrains.skija.IRect
import org.jetbrains.skija.Bitmap
import androidx.compose.ui.graphics.drawscope.drawCanvas
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
//EXPERIMENTAL FOCUS API
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.ui.focus
import androidx.compose.ui.focus.ExperimentalFocus
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.isFocused
import androidx.compose.ui.focusObserver
import androidx.compose.ui.focusRequester
import androidx.compose.foundation.clickable
private val width = mutableStateOf(0)
private val height = mutableStateOf(0)
private val x = mutableStateOf(0)
private val y = mutableStateOf(0)
@Composable
fun CefView() {
CefLayout(BrowserState)
}
@Composable
fun CefLayout(browser: BrowserState) {
var bitmap by mutableStateOf(browser.getBitmap())
browser.onInvalidate(
onInvalidate = {
bitmap = browser.getBitmap()
})
CefCanvas(bitmap, browser)
onActive {
browser.onActive()
}
onDispose {
browser.onDismiss()
}
}
@Composable
@OptIn(
ExperimentalFocus::class,
ExperimentalFoundationApi::class
)
fun CefCanvas(bitmap: Bitmap, browser: BrowserState) {
val focusRequester = FocusRequester()
Canvas(
modifier = Modifier
.fillMaxSize()
.onResized(browser)
.onPositioned { coordinates ->
x.value = coordinates.globalPosition.x.toInt()
y.value = coordinates.globalPosition.y.toInt()
}
.focusRequester(focusRequester)
.focusObserver { browser.setFocused(it.isFocused) }
.focus()
.clickable(indication = null) { focusRequester.requestFocus() }
) {
drawCanvas { canvas, size ->
canvas.nativeCanvas.drawBitmapRect(bitmap, IRect(0, 0, width.value, height.value).toRect())
}
}
}
private fun Modifier.onResized(browser: BrowserState) = Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
width.value = placeable.width
height.value = placeable.height
browser.onLayout(x.value, y.value, width.value, height.value)
layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}

152
cef/src/main/kotlin/org/jetbrains/compose/desktop/browser/ComposeBrowserWrapper.kt

@ -0,0 +1,152 @@
package org.jetbrains.compose.desktop.browser
import androidx.compose.ui.unit.IntOffset
import java.awt.event.KeyEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.event.MouseMotionAdapter
import java.awt.KeyboardFocusManager
import java.nio.ByteBuffer
import org.cef.CefApp
import org.cef.CefClient
import org.cef.CefSettings
import org.cef.browser.CefBrowser
import org.cef.browser.BrowserView
import org.cef.handler.CefFocusHandlerAdapter
import org.jetbrains.skija.Bitmap
import org.jetbrains.skiko.HardwareLayer
open class ComposeBrowserWrapper {
private var offset = IntOffset(0, 0)
private var isFocused = false
private var cefFocus = true
private val browser: BrowserView
public var onInvalidate: (() -> Unit)? = null
constructor(layer: HardwareLayer, startURL: String) {
if (!CefApp.startup(arrayOf(""))) {
throw Error("CEF initialization failed!")
}
val settings = CefSettings()
settings.windowless_rendering_enabled = true
val cefApp = CefApp.getInstance(settings)
val client = cefApp.createClient()
browser = object : BrowserView(layer, client, startURL, null) {
public override fun onBitmapChanged(popup: Boolean, buffer: ByteBuffer, width: Int, height: Int) {
super.onBitmapChanged(popup, buffer, width, height)
onInvalidate?.invoke()
}
}
client.addFocusHandler(object : CefFocusHandlerAdapter() {
public override fun onGotFocus(cefBrowser: CefBrowser) {
if (cefFocus) {
return
}
cefFocus = true
KeyboardFocusManager.getCurrentKeyboardFocusManager().clearGlobalFocusOwner()
browser.onFocusGained()
}
public override fun onTakeFocus(cefBrowser: CefBrowser, next: Boolean) {
cefFocus = false
browser.onFocusLost()
}
})
layer.addMouseListener(object : MouseAdapter() {
override fun mousePressed(event: MouseEvent) {
if (isInLayer(event))
browser.onMouseEvent(event)
}
override fun mouseReleased(event: MouseEvent) {
if (isInLayer(event))
browser.onMouseEvent(event)
}
})
layer.addMouseMotionListener(object : MouseMotionAdapter() {
override fun mouseMoved(event: MouseEvent) {
if (isInLayer(event))
browser.onMouseEvent(event)
}
override fun mouseDragged(event: MouseEvent) {
if (isInLayer(event))
browser.onMouseEvent(event)
}
})
layer.addMouseWheelListener(object : MouseWheelListener {
override fun mouseWheelMoved(event: MouseWheelEvent) {
if (isInLayer(event))
browser.onMouseScrollEvent(event)
}
})
layer.addKeyListener(object : KeyAdapter() {
override fun keyPressed(event: KeyEvent) {
if (!isFocused) {
return
}
browser.onKeyEvent(event)
}
override fun keyReleased(event: KeyEvent) {
if (!isFocused) {
return
}
browser.onKeyEvent(event)
}
override fun keyTyped(event: KeyEvent) {
if (!isFocused) {
return
}
browser.onKeyEvent(event)
}
})
}
private fun isInLayer(event: MouseEvent): Boolean {
val x = event.x
val y = event.y
if (x > offset.x && y > offset.y) {
return true
}
return false
}
fun loadURL(url: String) {
browser.loadURL(url)
}
fun setFocused(value: Boolean) {
isFocused = value
}
fun getBitmap(): Bitmap {
return browser.getBitmap()
}
fun onLayout(x: Int, y: Int, width: Int, height: Int) {
offset = IntOffset(x, y)
browser.onResized(x, y, width, height)
}
fun onActive() {
browser.onStart()
}
fun onDismiss() {
CefApp.getInstance().dispose()
}
}

483
cef/third_party/java-cef-jb-compose-patch/jb_compose_support.patch vendored

@ -0,0 +1,483 @@
diff --git a/build.xml b/build.xml
index ce2d8d7..d0afff6 100644
--- a/build.xml
+++ b/build.xml
@@ -18,6 +18,10 @@
<include name="gluegen-rt-natives-macosx-universal.jar"/>
<include name="jogl-all.jar"/>
<include name="jogl-all-natives-macosx-universal.jar"/>
+ <!-- SKIKO: copy skiko and kotlin-stdlib artifacts to "third_party/jogamp/jar" directory -->
+ <include name="skiko-jvm-0.1.6.jar"/>
+ <include name="kotlin-stdlib-1.4.0.jar"/>
+ <!-- SKIKO -->
</fileset>
<fileset dir="third_party/junit" includes="junit*.jar"/>
</path>
@@ -71,6 +75,10 @@
<include name="gluegen-rt-natives-macosx-universal.jar" />
<include name="jogl-all.jar" />
<include name="jogl-all-natives-macosx-universal.jar" />
+ <!-- SKIKO: copy skiko and kotlin-stdlib artifacts to "third_party/jogamp/jar" directory -->
+ <include name="skiko-jvm-0.1.6.jar"/>
+ <include name="kotlin-stdlib-1.4.0.jar"/>
+ <!-- SKIKO -->
</classpath>
<classpath dir="third_party/junit" includes="junit*.jar"/>
<option value="-Djava.library.path=$APP_ROOT/Contents/Java/:$APP_ROOT/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries" />
diff --git a/java/org/cef/browser/BrowserDropTargetListener.java b/java/org/cef/browser/BrowserDropTargetListener.java
new file mode 100644
index 0000000..5baaa86
--- /dev/null
+++ b/java/org/cef/browser/BrowserDropTargetListener.java
@@ -0,0 +1,123 @@
+// Copyright (c) 2019 The Chromium Embedded Framework Authors. All rights
+// reserved. Use of this source code is governed by a BSD-style license that
+// can be found in the LICENSE file.
+
+package org.cef.browser;
+
+import org.cef.callback.CefDragData;
+import org.cef.misc.EventFlags;
+
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.Transferable;
+import java.awt.dnd.DnDConstants;
+import java.awt.dnd.DropTargetDragEvent;
+import java.awt.dnd.DropTargetDropEvent;
+import java.awt.dnd.DropTargetEvent;
+import java.awt.dnd.DropTargetListener;
+import java.io.File;
+import java.util.List;
+
+class BrowserDropTargetListener implements DropTargetListener {
+ private BrowserView browser_;
+ private CefDragData dragData_ = null;
+ private int dragOperations_ = CefDragData.DragOperations.DRAG_OPERATION_COPY;
+ private int dragModifiers_ = EventFlags.EVENTFLAG_NONE;
+ private int acceptOperations_ = DnDConstants.ACTION_COPY;
+
+ BrowserDropTargetListener(BrowserView browser) {
+ browser_ = browser;
+ }
+
+ @Override
+ public void dragEnter(DropTargetDragEvent event) {
+ CreateDragData(event);
+ browser_.dragTargetDragEnter(
+ dragData_, event.getLocation(), dragModifiers_, dragOperations_);
+ }
+
+ @Override
+ public void dragExit(DropTargetEvent event) {
+ AssertDragData();
+ browser_.dragTargetDragLeave();
+ ClearDragData();
+ }
+
+ @Override
+ public void dragOver(DropTargetDragEvent event) {
+ AssertDragData();
+ browser_.dragTargetDragOver(event.getLocation(), dragModifiers_, dragOperations_);
+ }
+
+ @Override
+ public void dropActionChanged(DropTargetDragEvent event) {
+ AssertDragData();
+ acceptOperations_ = event.getDropAction();
+ switch (acceptOperations_) {
+ case DnDConstants.ACTION_LINK:
+ dragOperations_ = CefDragData.DragOperations.DRAG_OPERATION_LINK;
+ dragModifiers_ =
+ EventFlags.EVENTFLAG_CONTROL_DOWN | EventFlags.EVENTFLAG_SHIFT_DOWN;
+ break;
+ case DnDConstants.ACTION_COPY:
+ dragOperations_ = CefDragData.DragOperations.DRAG_OPERATION_COPY;
+ dragModifiers_ = EventFlags.EVENTFLAG_CONTROL_DOWN;
+ break;
+ case DnDConstants.ACTION_MOVE:
+ dragOperations_ = CefDragData.DragOperations.DRAG_OPERATION_MOVE;
+ dragModifiers_ = EventFlags.EVENTFLAG_SHIFT_DOWN;
+ break;
+ case DnDConstants.ACTION_NONE:
+ // The user did not select an action, so use COPY as the default.
+ dragOperations_ = CefDragData.DragOperations.DRAG_OPERATION_COPY;
+ dragModifiers_ = EventFlags.EVENTFLAG_NONE;
+ acceptOperations_ = DnDConstants.ACTION_COPY;
+ break;
+ }
+ }
+
+ @Override
+ public void drop(DropTargetDropEvent event) {
+ AssertDragData();
+ browser_.dragTargetDrop(event.getLocation(), dragModifiers_);
+ event.acceptDrop(acceptOperations_);
+ event.dropComplete(true);
+ ClearDragData();
+ }
+
+ private void CreateDragData(DropTargetDragEvent event) {
+ assert dragData_ == null;
+ dragData_ = createDragData(event);
+ dropActionChanged(event);
+ }
+
+ private void AssertDragData() {
+ assert dragData_ != null;
+ }
+
+ private void ClearDragData() {
+ dragData_ = null;
+ }
+
+ private static CefDragData createDragData(DropTargetDragEvent event) {
+ CefDragData dragData = CefDragData.create();
+
+ Transferable transferable = event.getTransferable();
+ DataFlavor[] flavors = transferable.getTransferDataFlavors();
+ for (DataFlavor flavor : flavors) {
+ try {
+ // TODO(JCEF): Add support for other flavor types.
+ if (flavor.isFlavorJavaFileListType()) {
+ List<File> files = (List<File>) transferable.getTransferData(flavor);
+ for (File file : files) {
+ dragData.addFile(file.getPath(), file.getName());
+ }
+ }
+ } catch (Exception e) {
+ // Data is no longer available or of unsupported flavor.
+ e.printStackTrace();
+ }
+ }
+
+ return dragData;
+ }
+}
diff --git a/java/org/cef/browser/BrowserView.java b/java/org/cef/browser/BrowserView.java
new file mode 100644
index 0000000..fdc44ac
--- /dev/null
+++ b/java/org/cef/browser/BrowserView.java
@@ -0,0 +1,209 @@
+package org.cef.browser;
+
+import org.cef.CefClient;
+import org.cef.callback.CefDragData;
+import org.cef.handler.CefRenderHandler;
+import org.cef.handler.CefScreenInfo;
+
+import java.awt.Component;
+import java.awt.Cursor;
+import java.awt.Graphics;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.dnd.DropTarget;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+import java.awt.event.MouseWheelEvent;
+import java.awt.event.MouseWheelListener;
+import java.awt.image.BufferedImage;
+import java.util.concurrent.CompletableFuture;
+import java.nio.ByteBuffer;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.swing.MenuSelectionManager;
+import javax.swing.SwingUtilities;
+import javax.swing.JFrame;
+
+import org.jetbrains.skija.Bitmap;
+import org.jetbrains.skiko.OpenGLApi;
+import org.jetbrains.skiko.HardwareLayer;
+
+public class BrowserView extends CefBrowser_N implements CefRenderHandler {
+ private BrowserViewBitmap bitmapHandler;
+ private HardwareLayer canvas;
+ private long windowHandle = 0;
+ private Rectangle browserRect = new Rectangle(0, 0, 1, 1); // Work around CEF issue #1437.
+ private Point screenPoint = new Point(0, 0);
+ private Lock locker = new ReentrantLock();
+
+ public BrowserView(HardwareLayer canvas, CefClient client, String url, CefRequestContext context) {
+ this(canvas, client, url, context, null, null);
+ }
+
+ private BrowserView(HardwareLayer canvas, CefClient client, String url, CefRequestContext context,
+ BrowserView parent, Point inspectAt) {
+ super(client, url, context, parent, inspectAt);
+ this.canvas = canvas;
+ bitmapHandler = new BrowserViewBitmap();
+ new DropTarget(canvas, new BrowserDropTargetListener(this));
+ }
+
+ @Override
+ public void createImmediately() {
+ createBrowserIfRequired(false);
+ }
+
+ @Override
+ public Component getUIComponent() {
+ return canvas;
+ }
+
+ @Override
+ public CefRenderHandler getRenderHandler() {
+ return this;
+ }
+
+ @Override
+ protected CefBrowser_N createDevToolsBrowser(CefClient client, String url, CefRequestContext context,
+ CefBrowser_N parent, Point inspectAt) {
+ return new BrowserView(canvas, client, url, context, (BrowserView) this, inspectAt);
+ }
+
+ @Override
+ public Rectangle getViewRect(CefBrowser browser) {
+ return browserRect;
+ }
+
+ @Override
+ public Point getScreenPoint(CefBrowser browser, Point viewPoint) {
+ Point sp = new Point(screenPoint);
+ sp.translate(viewPoint.x, viewPoint.y);
+ return sp;
+ }
+
+ @Override
+ public void onPopupShow(CefBrowser browser, boolean show) {
+ if (!show) {
+ bitmapHandler.clearPopupRects();
+ invalidate();
+ }
+ }
+
+ @Override
+ public void onPopupSize(CefBrowser browser, Rectangle size) {
+ bitmapHandler.onPopupSize(size);
+ }
+
+ @Override
+ public void onPaint(CefBrowser browser, boolean popup, Rectangle[] dirtyRects, ByteBuffer buffer, int width,
+ int height) {
+ onBitmapChanged(popup, buffer, width, height);
+ }
+
+ public void onBitmapChanged(boolean popup, ByteBuffer buffer, int width, int height) {
+ bitmapHandler.setBitmapData(popup, buffer, width, height);
+ }
+
+ @Override
+ public void onCursorChange(CefBrowser browser, final int cursorType) {
+ SwingUtilities.invokeLater(() -> {
+ canvas.setCursor(new Cursor(cursorType));
+ });
+ }
+
+ @Override
+ public boolean startDragging(CefBrowser browser, CefDragData dragData, int mask, int x, int y) {
+ return false;
+ }
+
+ @Override
+ public void updateDragCursor(CefBrowser browser, int operation) {
+ }
+
+ public void onMouseEvent(MouseEvent event) {
+ event.translatePoint(-browserRect.x, -browserRect.y);
+ sendMouseEvent(event);
+ }
+
+ public void onMouseScrollEvent(MouseWheelEvent event) {
+ event.translatePoint(browserRect.x, browserRect.y);
+ sendMouseWheelEvent(event);
+ }
+
+ public void onKeyEvent(KeyEvent event) {
+ sendKeyEvent(event);
+ }
+
+ public void onResized(int x, int y, int width, int height) {
+ browserRect.setBounds(x, y, width, height);
+ screenPoint = canvas.getLocationOnScreen();
+ wasResized(width, height);
+ }
+
+ public void onFocusGained() {
+ if (windowHandle != 0) {
+ MenuSelectionManager.defaultManager().clearSelectedPath();
+ setFocus(true);
+ }
+ }
+
+ public void onFocusLost() {
+ if (windowHandle != 0) {
+ setFocus(false);
+ }
+ }
+
+ public void dispose() {
+ bitmapHandler.clean();
+ }
+
+ public void onStart() {
+ SwingUtilities.invokeLater(() -> {
+ createBrowserIfRequired(true);
+ });
+ }
+
+ public Bitmap getBitmap() {
+ return bitmapHandler.getBitmap();
+ }
+
+ private void createBrowserIfRequired(boolean hasParent) {
+ long windowHandle = 0;
+ if (hasParent) {
+ windowHandle = getWindowHandle();
+ }
+ if (getNativeRef("CefBrowser") == 0) {
+ if (getParentBrowser() != null) {
+ createDevTools(getParentBrowser(), getClient(), windowHandle, true, false, null, getInspectAt());
+ } else {
+ createBrowser(getClient(), windowHandle, getUrl(), true, false, null, getRequestContext());
+ }
+ } else {
+ setFocus(true);
+ }
+ }
+
+ private synchronized long getWindowHandle() {
+ if (windowHandle == 0) {
+ windowHandle = canvas.getWindowHandle();
+ }
+ return windowHandle;
+ }
+
+ @Override
+ public CompletableFuture<BufferedImage> createScreenshot(boolean nativeResolution) {
+ throw new UnsupportedOperationException("BrowserView:createScreenshot - Not implemented, yet.\n");
+ }
+
+ @Override
+ public boolean getScreenInfo(CefBrowser browser, CefScreenInfo screenInfo) {
+ // TODO Auto-generated method stub
+ return false;
+ }
+}
diff --git a/java/org/cef/browser/BrowserViewBitmap.java b/java/org/cef/browser/BrowserViewBitmap.java
new file mode 100644
index 0000000..036075b
--- /dev/null
+++ b/java/org/cef/browser/BrowserViewBitmap.java
@@ -0,0 +1,107 @@
+package org.cef.browser;
+
+import java.awt.Rectangle;
+import java.nio.ByteBuffer;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.jetbrains.skija.ColorAlphaType;
+import org.jetbrains.skija.Bitmap;
+import org.jetbrains.skija.ImageInfo;
+
+class BrowserViewBitmap {
+
+ private Bitmap bitmap = null;
+ private byte[] pixels = null;
+ private int width = 0;
+ private int height = 0;
+ private Rectangle popupRect = new Rectangle(0, 0, 0, 0);
+ private Rectangle popupOriginRect = new Rectangle(0, 0, 0, 0);
+ private boolean popup = false;
+
+ private Lock lock = new ReentrantLock();
+
+ void clean() {
+ width = height = 0;
+ }
+
+ private byte[] getBytes(ByteBuffer buffer, int width, int height) {
+ byte[] pixels = new byte[buffer.capacity()];
+ int index = 0;
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ pixels[index++] = buffer.get((x + y * width) * 4 + 0);
+ pixels[index++] = buffer.get((x + y * width) * 4 + 1);
+ pixels[index++] = buffer.get((x + y * width) * 4 + 2);
+ pixels[index++] = buffer.get((x + y * width) * 4 + 3);
+ }
+ }
+ return pixels;
+ }
+
+ Bitmap getBitmap() {
+ lock.lock();
+ try {
+ if (bitmap == null) {
+ init();
+ }
+ return bitmap;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ void init() {
+ bitmap = new Bitmap();
+ bitmap.allocPixels(ImageInfo.makeS32((int) width, (int) height, ColorAlphaType.PREMUL));
+ }
+
+ void setBitmapData(boolean popup, ByteBuffer buffer, int width, int height) {
+ lock.lock();
+ try {
+ this.popup = popup;
+ if (this.width != width || this.height != height) {
+ this.height = height;
+ this.width = width;
+ init();
+ }
+ pixels = getBytes(buffer, width, height);
+ bitmap.installPixels(bitmap.getImageInfo(), pixels, width * 4);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ void onPopupSize(Rectangle rect) {
+ if (rect.width <= 0 || rect.height <= 0)
+ return;
+ popupOriginRect = rect;
+ popupRect = getPopupRectInWebView(popupOriginRect);
+ }
+
+ Rectangle getPopupRect() {
+ return (Rectangle) popupRect.clone();
+ }
+
+ Rectangle getPopupRectInWebView(Rectangle originalRect) {
+ Rectangle rc = originalRect;
+ if (rc.x < 0)
+ rc.x = 0;
+ if (rc.y < 0)
+ rc.y = 0;
+ if (rc.x + rc.width > width)
+ rc.x = width - rc.width;
+ if (rc.y + rc.height > height)
+ rc.y = height - rc.height;
+ if (rc.x < 0)
+ rc.x = 0;
+ if (rc.y < 0)
+ rc.y = 0;
+ return rc;
+ }
+
+ void clearPopupRects() {
+ popupRect.setBounds(0, 0, 0, 0);
+ popupOriginRect.setBounds(0, 0, 0, 0);
+ }
+}
Loading…
Cancel
Save