JavaFX の WebView に文字列検索機能を付けてみる

このエントリは JavaFX Advent Calendar 2014 の 19 日目のエントリです。前日は id:yumix_h さんによる、「WebView(JavaFX)のズーム機能を使ってみました」でした。

はじめに

JavaFX には Web ブラウザコンポーネントである WebView が付いているのは皆さんよくご存じかと思います。
DOM へのアクセス、JavaScript 実行、ヒストリ情報へのアクセス、JavaScript イベントのフック、など様々な機能が提供されており、他の GUI ツールキットが提供するブラウザコンポーネントと比較しても遜色がないと思っています。

ただ、残念なことに WebView 上に表示されているコンテンツに対してテキスト検索を行うための API が用意されていないんですよね。
もちろん表示されているコンテンツの DOM にアクセス可能なので、その DOM を頑張ってパースするという手もあるのですが、中々辛い。

というわけで、既存の JavaScript ライブラリを使って手っ取り早く実装する方法を考えてみました。

作ってみたもの

次の図のように、WebView を使ったブラウザアプリケーションに検索ボックスを取り付け、検索ボックスに文字列を入力するとマッチする部分をハイライトするようにしました。

マッチした部分をハイライトするだけで、ハイライトした箇所にスクロールする機能はありません。

ちなみに、このスクリーンショットは、自分が作った Social Bookmark Viewer FX という、Diigo というソーシャルブックマークサービスのビューアアプリケーションの一部です。 *1

利用した JavaScript ライブラリ

この機能を実現するために次のライブラリを利用してみました。

JavaScript text higlighting jQuery plugin
http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html

jQueryプラグインとして提供されているライブラリです。
使い方としては、予め以下の CSS クラスを定義しておき、

.highlight { background-color: yellow }

あとは次のようにこのプラグインjQuery オブジェクトに生える highlight() メソッドを、ハイライトしたい文字列を引数に渡して呼べば OK です。

$('body').highlight('foo');

WebView に検索機能を追加する

それではこれを WebView で使えるようにします。まず、WebView 上のドキュメントのロードに成功したら、jQuery 及び上で紹介した検索ライブラリをドキュメントに追加します。

final WebEngine webEngine = webView.getEngine();
final Worker<Void> loadWorker = webEngine.getLoadWorker();
loadWorker.stateProperty().addListener((observable, oldValue, newValue) -> {
    if (newValue == Worker.State.SUCCEEDED) {
        // 読み込みが成功した場合、検索用プラグインを読み込む
        Optional.ofNullable(webEngine.getDocument()).ifPresent(document -> {
            addScriptElement(document, "//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"); // (1)
            addScriptElement(document, "http://johannburkard.de/resources/Johann/jquery.highlight-4.closure.js"); // (2)
            addStyleElement(document, ".highlight { background-color: yellow; }"); // (3)
        });
    }
});

WebEngine の読み込み状態を観察し、成功した場合にのみ検索用プラグインを読み込むようにしています。
(1) で jQuery を、(2) で検索ライブラリを、(3) で検索ライブラリが要求するスタイルクラスを DOM に追加しています。
JavaScript ライブラリはどちらもネットワークから直接読み込んでいます。

addScriptElement() メソッド及び addStyleElement() の実装は次のようになっています。

private void addScriptElement(Document document, String url) {
    final Element scriptElm = document.createElement("script");
    scriptElm.setAttribute("type", "text/javascript");
    scriptElm.setAttribute("src", url);
    NodeList bodys = document.getElementsByTagName("body");
    if (bodys != null && bodys.getLength() > 0) {
        bodys.item(0).appendChild(scriptElm);
    }
}

private void addStyleElement(Document document, String styleContent) {
    final Element styleElm = document.createElement("style");
    styleElm.setAttribute("type", "text/css");
    styleElm.setTextContent(styleContent);
    NodeList bodys = document.getElementsByTagName("body");
    if (bodys != null && bodys.getLength() > 0) {
        bodys.item(0).appendChild(styleElm);
    }
}

普通に DOM API を用いて、script 要素や style 要素を作ってドキュメントに追加しているのが分かりますね。

続いて検索テキストボックスに入力があったら、ハイライトする処理です。

final TextField pageSearchBox = new TextField();
pageSearchBox.textProperty().addListener(
        (observable, oldValue, newValue) -> highlightpage(Optional.ofNullable(newValue)));

TextField の text プロパティの変化時に highlightpage() メソッドを呼び出しています。
highlightpage() メソッドの実装は次のようになっています。

private void highlightpage(Optional<String> word) {
    Optional.ofNullable(webEngine.getDocument()).ifPresent(document -> {
        final String keyword = word.orElse("");
        webEngine.executeScript("$('body').removeHighlight()");
        if (!keyword.isEmpty()) {
            webEngine.executeScript("$('body').removeHighlight().highlight('" + keyword + "')");
        }
    });
}

WebEngine#executeScript() メソッドを使って、ハイライトを行う JavaScript ライブラリのメソッドをコールしています。

実装は以上です。これで上のスクリーンショットのような機能を実現できます。
JavaScript ライブラリの手助けで割と簡単にできましたね。これも JavaFX の WebView/WebEngine が DOM へのアクセスや JavaScript の実行を行える仕組みを提供してくれているからです。
ただし、この方法だと script 要素を追加して余計な JavaScript ライブラリを読み込ませたり、新たなスタイル定義を追加したりしているので、元々コンテンツがロードしている JavaScript コードやスタイル定義とバッティングする危険性があることに注意してください。 *2
重要度の高いアプリケーションでは JavaScript のスコープなどをより慎重に設計する必要があります。

明日は id:kikutaro777 さんの予定です。

*1:そのうちこれ自身の紹介エントリも書きたいと思っています。

*2:実際いくつかのサイトで不具合を起こしているのを確認しました

Yosemiteで動かなかったソフトウェア

昨日のエントリの続きです。Yosemite にすると、案の定動かなくなったソフトウェアも出てきました。
以下のソフトウェアです。

いずれも最新バージョン (Kaspersky は 15、VMWare は 7) に更新することで動作しました。Kaspersky はライセンス保持者に対してはアップグレードが無料ですが、VMWare Fusion の方は有料です (ライセンス保持者は優待価格で購入できますが) 。 *1

流石にセキュリティソフトウェアや仮想化ソフトウェアのようなシステムの深いところに触りそうなものは影響受けますね。
今までの経験からすると大体 2 バージョンくらいしか持ちません。OS が無料でもこういうところで結局金が掛かるんですよね。
このようにしてサードパーティーソフトウェアに対しても最新版の購入を強要させる辺り、Apple はホンマ ISV 思いですねえ......クソが。

*1:最近 VMWare 製品の購入経路は VMWare の直販のみになりました。なので、VMWare の Web サイトから購入することになりますが、購入手順が非常に分かり辛かったですよ...。

Yosemiteで遭遇した問題

Mac OSX の最新版、Yosemite が登場しましたが、Time Machine でバックアップも取っていることもあり、早速突撃してみました。
Yosemite は一新されたデザインが結構自分の好みに合っているんですよねー。

これまで Apple 系 OS でアップグレードとかしてもトラブルに巻き込まれることはそれほど無かったのですが (一度 iPhone のアップグレードでバックアップからの復帰に失敗するというバグを踏んだことあり) 、今回は色々地雷を踏んでしまいました。
ということで以下に参考までに列挙しておきます。
なお、使用しているマシンは MacBook Pro 15inch の Mid2010 モデルです。LED Cinema Display につないでデュアルディスプレイにして使っています。

Google Drive が起動しない (対応策あり)

Google Driveツールバーアイコンがグレーアウトし、クリックにも反応しないという現象が起きました。マウスカーソルをホバーさせると、「ログインしていません」というメッセージが出ます。
最新バージョンをダウンロードして再インストールしても状況は同じでした。

こちらについては調べてみると以下のサイトに解決策が記載されており、その手順に従うと解決しました。
http://ralfrottmann.net/fix-google-drive-mac-app-on-yosemite

IM を起動しての文字入力が遅くなる (Safari がトリガ?)

IM を起動しての日本語の入力が遅いという問題です。文字を入力する際に頻繁にツールバーがちらつき、レスポンスが遅くなるという現象で、これはかなりイラつきます。
私は普段 ATOK を使っていますが、Yosemiteことえりに代わって新たに導入された JapaneseIM でも現象は同じです。
この現象は常に起きるわけではなく、どうも Safari を起動し、そこでテキストを入力することがトリガとなって発生するように見えます。
なぜ Safari なのか、なぜ Safari で問題が起きると、それ以降はどのアプリケーションでも入力が遅くなるのか、非常に謎な事象です。

デュアルディスプレイだとサブディスプレイ側の描画が乱れる

上にも書きましたが、MBP に LED Cinema Display をつないでデュアルディスプレイにしていますが (LED Cinema Display 側をプライマリにしている) 、サブディスプレイの描画が乱れることがあります。
再現条件がはっきりしていませんが、これも Safari の実行がトリガとなって発生するように見えます。

こんな感じで、ちょっと触っただけでも結構色々ありました。仕事用など、大事なマシンに入れるのはまだ様子を見た方がいいように思えます。
どうも新しい Safari が困ったちゃんな感じに見えますねえ。操作も少し重たいし (Web ページのレンダリング自体は速いのですが) 。

Mac の不具合報告は以下のフォームから行えるようなので、これからフィードバックします。
https://www.apple.com/jp/feedback/macosx.html

(2014/12/08追記)

本エントリで報告した問題のうち、後ろ 2 つの「IM を起動しての文字入力が遅くなる (Safari がトリガ?)」「デュアルディスプレイだとサブディスプレイ側の描画が乱れる」については解決策がありました。
次のようにアクセシビリティの設定で「コントラストを上げる」「透明度を下げる」の設定をONにすると解決しました。

やはり半透明のエフェクトが非常に高負荷だったようです。

Asakusa Framework 勉強会 2014真夏でLTを行ってきました

8/22 に実施された Asakusa Framework 勉強会 2014真夏 で LT を行ってきましたので、発表内容の補足と勉強会の感想について書きたいと思います。

当日の発表資料は以下の通りです。

AsakusaのドキュメントをDashで見たい from Takashi Aoe

直前に発表することを決めたので、内容は完全に小ネタです。
Mac 用の API ドキュメント閲覧ツールである Dash の紹介と、その Dash に Asakusa Framework の公式ドキュメントAPI ドキュメントを組み込めるようにする方法について紹介しました。
その説明を通して、Asakusa の公式ドキュメント自身も GitHub 上で公開されていること、ドキュメントも OSS として公開されている以上、みんながその内容を充実させるための貢献ができることを伝えることも狙いでした。
図らずも今回の LT 大会では公式ドキュメントに対する指摘が色々ありました。OSS への参加で一番貢献しやすいものの一つがドキュメントだと思っています。中の人的にはドキュメントに対して貢献してもらうことはとても有り難いことなので、どしどし指摘なりパッチなりを送って欲しいと思います。
現在 Issue がみんな英語で記述されているので、躊躇されている方もいらっしゃるかも知れませんが、日本語で全然おっけーですので、どんどん寄せてください。

LT で紹介した Dash はとても良いツールなので、Mac を使っている開発者の皆様は是非試してみてください。
様々な言語やライブラリの API ドキュメントを 1 つのツールで一元的に閲覧できるのは便利です。
それに、第 3 者がコントリビュートできるようになっているため、かなりマニアックなものまでカバーしています。
以下に Dash の Web ページで紹介されている、サポートしている言語やライブラリの一覧のスクリーンショットを貼っておきますが (これでも上半分です) 、実に様々なものをカバーしていることが分かりますね。 *1

さて、他の方の発表を聴いていての感想ですが、皆さんかなり Asakusa をガッツリと使い出しているなあと驚きました。
非構造化データの取り扱いへのチャレンジがあったり、演算子の使い方に対する深い考察をした発表があったり、テストドライバの実装を調査した発表もあったりと、こちらも聴いていて「そういう視点があったか」と何度か気付かされることがありました。
土佐さんの演算子チートシートはとてもいいですね!私も可能ならば内容の充実に貢献したいと思っています。 *2

*1:個人的にはちゃんと JavaFX もカバーされていたことがポイント大 :-)

*2:プロジェクトページは https://github.com/ironpeace/asakusa_operators_cheetsheet のようですね

JavaFXで丸型ボタンを作ってみる (FXML + CSS + ベクター画像縛り)

はじめに

VAIO Tap 11 を購入したこともあり、Windows8 をよく使うようになったわけですが、Windows8 の Store App を使っていると、次のようなタッチでの利用を意識した丸型で大きめのボタンをよく見かけるようになりました。

で、これと同じようなものを JavaFX でも作ってみようとしました。その際に次のような条件を課すことにしました。

  • FXML と CSS の範囲だけで作る。
    • Java コード側にデザインに関するコードを含めないようにする。
  • アイコンにはビットマップではなくベクターの画像を使う。
    • スケールできるようにしたい。

以下、手順について順に示していきます。

ボタンの作成

まず、ボタンの外形については CSS を使えば簡単に円形にすることができます。
またアイコンについては、JavaFX の Button クラスには graphic プロパティというものがあり、このプロパティに任意の Node を設定することでボタンに画像を表示できるようになります。

というわけでまずは CSS の準備です。ボタンに適用するクラスを作成します。

.circle-button {
    -fx-pref-width: 4.0em;
    -fx-pref-height: 4.0em;
    -fx-background-radius: 2.0em;
    -fx-background-color: null;
    -fx-border-radius: 2.0em;
    -fx-border-color: black;
    -fx-border-width: 2.0px;
}

ポイントは次の通り。

  • background (背景) と border (境界) の両方を調整する必要があります。
  • ボタンの幅、高さと、background、border の角の丸みを調整する radius プロパティを揃えることで円形にします。

さらにマウスオーバー、クリック、無効化の状態に対応するため、hover、pressed、disabled 疑似クラスのスタイルも設定します。

.circle-button:disabled {
    -fx-border-color: rgb(0, 0, 0, 0.7);
}
.circle-button:hover {
    -fx-background-color: #dcdcdc;
}
.circle-button:pressed {
    -fx-background-color: black;
    -fx-border-color: white;
}

無効化の場合は半透明にし、マウスオーバーの場合はうっすらと背景に色をつけるようにしています。クリックされた時には色を反転させています。

このクラスをボタンに適用することで次のような見た目のボタンになります。

アイコンの作成

続いてアイコンに使うベクター画像についてです。JavaFXjavafx.scene.shape パッケージにある各種クラスを利用することでベクター画像を作ることができます。

ですが、凝った画像を Java コードや FXML コードでごりごり書きたくないですよね。 *1
できれば IllustratorInkscape などのベクターグラフィックツールで作成した画像を使ったり、あるいは ネット上でフリーで配布されている素材とかを使いたいところです。

実は JavaFXSVG 形式のパスを取り扱うことができます。先ほど言及した javafx.scene.shape パッケージには SVGPath というクラスがあります。
また、CSS でも Region クラス (Control や Pane などのスーパークラス) には -fx-shape というプロパティがあり、SVG のパス文字列を指定することができます。

以上を踏まえて Scene Builder を使ってベクター形式のアイコン画像を準備する手順について示します。

まず、使いたい SVG 画像をテキストエディタで開き、そのパス文字列をコピーします。

Scene Builder で SVGPath オブジェクトを追加し、その Content プロパティに先ほどコピーしたパス文字列を貼り付けます。すると表示がそのアイコンに変わるはずです。

ボタンにアイコンを設定する

それではボタンに対して準備した画像を設定します。
FXML 上で Button オブジェクトの graphic プロパティに対して先ほど用意した SVGPath オブジェクトを次のように設定すれば OK です。

<Button layoutX="162.0" layoutY="51.0" mnemonicParsing="false" styleClass="circle-button">
   <graphic>
      <SVGPath content="M10,16 10,0 0,8z" styleClass="button-icon-shape" />
   </graphic>
</Button>

これを Scene Builder 上で簡単に行う方法があります。次のように、Hierarchy ビューで Button に対して Shape オブジェクト (ここでは SVGPath オブジェクト) をドラッグアンドドロップすることができ、こうすると、Buttton の graphic プロパティにその Shape オブジェクトが設定されるようになります。

ちなみに CSS では Button クラス (正確には基底クラスの Labeled クラス) に -fx-graphic というプロパティがありますが、このプロパティには URI しか指定できないため、ビットマップ画像のパスを指定するしかありません。

イコン画像もマウスクリックに反応させて色を変えるようにする

これで大体できあがりなのですが、先ほどボタンの疑似クラス設定で、マウスクリック時に色を反転させるように設定したことを覚えているでしょうか?
イコン画像についてもそれに追随させて色を反転させることを考えます。

ここは CSS の機能を利用して実現します。次のように子孫セレクタを使って、クリックされたボタンの子孫に当たる SVGPath に対して、色を反転させる設定が適用されるようにしています。

.circle-button:pressed SVGPath {
    -fx-fill: white;
}

できあがりの例

こんな感じで JavaFXWindows Store App にあるような丸型ボタンを作ることができました。
以下に完成例のスクリーンショットを載せておきます (アイコンの画像は このサイト で配布されているものを使わせてもらいました) 。

この作成例のソースコードは gist にアップしてあります。
https://gist.github.com/aoetk/b5b9a03e1033057224aa

*1:Scene Builder がもうちょっとお絵かきツールとしての機能も強化してくれるといいのですが...。

きしださんの文字列連結のやつをCharBufferでやってみる

多分色んな人が既にやっていて何番煎じになっているか分かりませんが、きしだ (id:nowokay / @) さんの「StringBuilderを使ったクソコードはどこまで遅いか 」「Java8時代の文字列連結まとめ」について自分もちょっと遊んでみました。

というのも今日の昼飯時に社内でもこの話が話題になって、そこで弊社の某モヒカンより「性能面でセンシティブな場面で String を使うことを考えるな。CharBuffer 使え。」との言葉を賜りましたからです。
と言うわけで以下のコードを試してみました。

    public static String charBufferJoin() {
        CharBuffer buffer = CharBuffer.allocate(7995);
        buffer.put('[');
        for (int i = 0; i < strarray.length; ++i) {
            if (i != 0) {
                buffer.put(',').put('[');
            }
            buffer.put(strarray[i]).put(']');
        }
        buffer.flip();
        return buffer.toString();
    }

結果は、

charBufferJoin:1261ms
stringJoin:2800ms
stringJoiner:2135ms
streamListJoin3:2362ms
streamListParallelJoin3:3677ms
stringBuilderJoin:1870ms
stringBuilderJoinMem:1652ms
stringBuilderFuckingJoin:2480ms

確かに速いっぽい。 *1 前述の某モヒカンのお言葉は正しかったようです。

おしまい。

*1:あと、ぼくのマシン、きしださんのマシンよりかなり遅いっぽい...。