[#1575] Support copy & paste (#1593)

* [#1575] Support paste

* WebView: Implement copy

* Localize copy dialog, lint

* Implement a custom context menu for copy/paste

* Remove click event which causes double events

* WebView: Fix input events broken by moved preventDefault

We want to fall back to the `input` event for Android bug and paste, but
we want to prevent the event for the others, so change the order
This commit is contained in:
Constantin Piber
2025-08-19 22:01:31 +03:00
committed by GitHub
parent 7a0d3a1efe
commit 3075888d26
4 changed files with 249 additions and 16 deletions

View File

@@ -27,7 +27,10 @@ import org.cef.network.CefCookieManager
import org.cef.network.CefRequest
import uy.kohesive.injekt.injectLazy
import java.awt.Component
import java.awt.HeadlessException
import java.awt.Rectangle
import java.awt.Toolkit
import java.awt.datatransfer.DataFlavor
import java.awt.event.InputEvent
import java.awt.event.KeyEvent
import java.awt.event.MouseEvent
@@ -113,6 +116,12 @@ class KcefWebView {
val error: String? = null,
) : Event()
@Serializable
@SerialName("copy")
private data class CopyEvent(
val content: String,
) : Event()
private inner class DisplayHandler : CefDisplayHandlerAdapter() {
override fun onConsoleMessage(
browser: CefBrowser,
@@ -346,6 +355,16 @@ class KcefWebView {
modifier: Int,
): KeyEvent? {
val char = if (msg.key?.length == 1) msg.key[0] else KeyEvent.CHAR_UNDEFINED
return keyEvent(char, component, id, modifier, msg.key)
}
private fun keyEvent(
char: Char,
component: Component,
id: Int,
modifier: Int,
strKey: String? = null,
): KeyEvent? {
val code =
when (char.uppercaseChar()) {
in 'A'..'Z', in '0'..'9' -> char.uppercaseChar().code
@@ -379,7 +398,7 @@ class KcefWebView {
' ' -> KeyEvent.VK_SPACE
'_' -> KeyEvent.VK_UNDERSCORE
else ->
when (msg.key) {
when (strKey) {
"Alt" -> KeyEvent.VK_ALT
"Backspace" -> KeyEvent.VK_BACK_SPACE
"Delete" -> KeyEvent.VK_DELETE
@@ -560,6 +579,39 @@ class KcefWebView {
}
}
fun paste(msg: String) {
val component = browser?.uiComponent ?: return
for (c in msg) {
browser!!.sendKeyEvent(keyEvent(c, component, KeyEvent.KEY_PRESSED, 0)!!)
keyEvent(c, component, KeyEvent.KEY_TYPED, 0)?.let { browser!!.sendKeyEvent(it) }
browser!!.sendKeyEvent(keyEvent(c, component, KeyEvent.KEY_RELEASED, 0)!!)
}
}
fun copy() {
val frame = browser?.focusedFrame ?: return
frame.copy()
val clip =
try {
Toolkit.getDefaultToolkit().getSystemClipboard()
} catch (e: HeadlessException) {
logger.warn(e) { "Failed to get clipboard" }
return
}
val text =
try {
clip.getData(DataFlavor.stringFlavor) as String
} catch (e: Exception) {
logger.warn(e) { "Failed to get clipboard contents" }
return
}
WebView.notifyAllClients(
Json.encodeToString<Event>(
CopyEvent(text),
),
)
}
fun canGoBack(): Boolean = browser!!.canGoBack()
fun goBack() {

View File

@@ -79,6 +79,16 @@ object WebView : Websocket<String>() {
val deltaY: Float? = null,
) : TypeObject()
@Serializable
@SerialName("paste")
data class JsPasteMessage(
val data: String,
) : TypeObject()
@Serializable
@SerialName("copy")
class JsCopyMessage : TypeObject()
override fun handleRequest(ctx: WsMessageContext) {
val dr = driver ?: return
try {
@@ -97,6 +107,12 @@ object WebView : Websocket<String>() {
is JsEventMessage -> {
dr.event(event)
}
is JsPasteMessage -> {
dr.paste(event.data)
}
is JsCopyMessage -> {
dr.copy()
}
}
} catch (e: Exception) {
logger.warn(e) { "Failed to deserialize client request: ${ctx.message()}" }