はじめに
windows での挙動は試していませんが、MacOS 上で JavaFX の WebView を使い、ace.js を組み込んだ際にテキストのコピーが動かない事象がありました。
環境は JavaFX は 13、ace.js は 1.4.8 ですが、JavaFX は 11 でも 14 でも同様です。
多少の情報は存在しますが、どれも上手く機能する対応ではなかったため、念の為ここに記しておきます。
Workaround 1
ユーザエージェントで Chrome18 以下を偽装することでテキストのコピーが可能となります。
WebView webView = new WebView(); WebEngine engine = webView.getEngine(); engine.userAgentProperty().setValue( engine.userAgentProperty().get() + " Chrome/17");
Workaround 2
この Workaround は上手く機能しないかもしれません。 手元の環境では、JavaFX13 だと動かなかったり、script をローカルファイルから読み込むか、CDNから取得するかで挙動が変わったりと、なかなか不安定だったのでおすすめではありません。
以下のようなアップコール用のブリッジを作成します。
public class UpCallBridge { public void copyUpCall(String text) { final Clipboard clipboard = Clipboard.getSystemClipboard(); final ClipboardContent content = new ClipboardContent(); content.putString(text); clipboard.setContent(content); } }
作成したブリッジを window
に登録します。
WebView webView = new WebView(); WebEngine engine = webView.getEngine(); engine.setJavaScriptEnabled(true); JSObject jsobj = (JSObject) engine .executeScript("window"); jsobj.setMember("java", new UpCallBridge());
Javascript 側で copy 時のリスナーとしてアップコールのブリッジを登録します。
editor.on('copy', function(obj) { java.copyUpCall(obj.text); });
Workaround 3
こちらは無理やりですが、コピー時のコールバックで例外を throw することでテキストのコピーが可能となります。
var editor = ace.edit("editor"); editor.on('copy', function(obj) { throw new Error('dummy'); });
ace.js におけるコピー処理
コピー処理は以下のような実装になっています。
var doCopy = function(e, isCut) { var data = host.getCopyText(); // (1-1) if (!data) return event.preventDefault(e); if (handleClipboardData(e, data)) { // (1-2) if (isIOS) { resetSelection(data); copied = data; setTimeout(function () { copied = false; }, 10); } isCut ? host.onCut() : host.onCopy(); event.preventDefault(e); // (1-3) } else { copied = true; text.value = data; text.select(); setTimeout(function(){ copied = false; resetSelection(); isCut ? host.onCut() : host.onCopy(); }); } };
(1-1)
で host
の getCopyText()
を呼び出し、コピー対象のテキストを取得しています。
(1-2)
でクリップボードへの保存を行い、(1-3)
で規定イベントをキャンセルしています。
(1-2)
でクリップボードへの保存が失敗した場合には preventDefault
は行いません。
(1-1)
に戻り、 host
は Editor
で、以下のようになっています。
this.getCopyText = function() { var text = this.getSelectedText(); // ... var e = {text: text}; this._signal("copy", e); // (2-1) clipboard.lineMode = copyLine ? e.text : ""; return e.text; };
選択中のテキストを取得し、(2-1)
でコールバック処理しています。editor.on('copy', function(obj) { });
とすることでコピー対象を受け取ることができます。
前述の Workaround 2 ではここにアップコール用のブリッジを登録しています。
前述の workaround 3 ではこのコールバック時に例外を発行し、ace.js でのコピー処理を無理やり行わないようにしたものです。
さて、(1-2)
での handleClipboardData
は以下のようになっています。
var handleClipboardData = function(e, data, forceIEMime) { var clipboardData = e.clipboardData || window.clipboardData; if (!clipboardData || BROKEN_SETDATA) // (3-1) return; var mime = USE_IE_MIME_TYPE || forceIEMime ? "Text" : "text/plain"; try { if (data) { return clipboardData.setData(mime, data) !== false; // (3-2) } else { return clipboardData.getData(mime); } } catch(e) { if (!forceIEMime) return handleClipboardData(e, data, true); } };
クリップボードへのコピーが動かない原因は (3-2)
の箇所で、クリップボードデータへ設定するも、システムのクリップボードへの反映が行われないためです。
詳細は追っていませんが、JavaFX の WebView では(nashornではなく) webkit の JavaScriptCore が使われており、ClipboardEvent.clipboardData
で取得した DataTransfer
オブジェクトへの設定は正常に行われますが、クリップボードには値が反映されないようです。
実際 clipboardData.setData()
で設定すれば、clipboardData.getData()
で値を取り出すことができますが、クリップボードには反映されていませんでした。
clipboardData の取得を以下のように(ClipboardEvent
からではなく) window
から取得したものを使えば上手く動きます。
var clipboardData = /* e.clipboardData || */ window.clipboardData;
(3-1)
で、BROKEN_SETDATA
であればこの処理をスキップしています。
BROKEN_SETDATA
は何かと言えば、以下のようにユーザエージェントで Chrome 18以下であることで初期化されています。
var BROKEN_SETDATA = useragent.isChrome < 18;
なので workaround 1 で示した方法で WebView のユーザエージェントを書き換えることで、動作しない、ClipboardEvent.clipboardData
への処理をスキップできます。
まとめ
話は前後しますが、ace.js を JavaFx で使うには以下のように利用できます(前述の Workaround 2 による対応版です)。
public class App extends Application { @Override public void start(Stage stage) { WebView webView = new WebView(); WebEngine engine = webView.getEngine(); engine.setJavaScriptEnabled(true); JSObject jsobj = (JSObject) engine .executeScript("window"); jsobj.setMember("java", new UpCallBridge()); engine.load(getClass().getClassLoader() .getResource("ace.html").toExternalForm()); Scene scene = new Scene(new StackPane(webView)); stage.setScene(scene); stage.show(); } public static void main(String[] args) { launch(); } }
アップコール用のブリッジです。
public class UpCallBridge { public void copyUpCall(String text) { final Clipboard clipboard = Clipboard.getSystemClipboard(); final ClipboardContent content = new ClipboardContent(); content.putString(text); clipboard.setContent(content); } }
ace.html
は以下のように作成します。
<!DOCTYPE html> <html> <head> <style type="text/css" media="screen"> #editor { position: absolute; top: 0; right: 0; bottom: 0; left: 0; } </style> </head> <body> <div id="editor"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.8/ace.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.8/theme-monokai.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.8/mode-java.min.js"></script> <script> var editor = ace.edit("editor"); editor.setTheme("ace/theme/monokai"); editor.getSession().setMode("ace/mode/java"); editor.on('copy', function(obj) { java.copyUpCall(obj.text); }); </script> </body> </html>