プログラムdeタマゴ

nodamushiの著作物は、文章、画像、プログラムにかかわらず全てUnlicenseです

JavaFX9からPlatformに追加されるAPIについて

 この記事はJavaFX Advent Calender 2016の17日目の記事になります。
 前日はid:skrbさんのSooner or Later - JavaFX in the Boxでした。
 明日はid:aoe-tkさんです。


 前回今年最後とか言ったな。あれは嘘だ。

 はい、というわけで、なんとJavaFX Advent Calender 2016に二度も顔出しですよ。この引きこもりの王者クソニート様が。誰かお仕事くらさい

 というのもですね、JavaFX9で追加される新機能は前回紹介した内容以外にも、PulseListenerの追加やPlatform.startupやら、他にも小さな機能の追加があるのです。これらの使い道が私には思いつかなかったか、そもそもあまり興味を持っていなかったかなどの理由でスルーしたのですが、実はスルーしてた中でPlatformにNested Event Loop関連のAPIが追加になっていたようなのです!

 いや、もうマジでこれ知らなかった。知ってたら前回の記事に絶対追加してた。というわけで、興奮してついうっかり2回も顔出しすることにしました。ほんとスマンかった。

 なお、このことはJavaFX: New & Noteworthy(※PDF)にも書いてあります。しかし、Nested Event Loopの話がPulseとPulse Listenerのページの間に挟まれているんですよね。Pulseとかあまり興味ないせいか、読むのをすっ飛ばした様で気がつかなかったんですね~。アホですね~。

 というわけで、Platformに追加されるAPI(主にNested Event Loop)について解説します。



Platform.startup

 Application.launch使う限り使わないと思われるAPI。SWTとかSwingから使うときとかを想定している模様。

 JavaFX Application Threadを生成直後に引数に渡したRunnableを実行するとかそんな感じっぽいけど、普通に使う場面はないと思う。ないよね?思いつかなかったから前回もスルーしたんだけど。





Platform.requestNextPulse

 Scene.addPostLayoutPulseListener,Scene.addPreLayoutPulseListenerと共に追加されたPulse関連のAPI。次のPulseの要求が出来る。

 JavaFXには我々が扱うJavaFX Application Threadの他にPrism Render Threadという、画面描画用のスレッドがあるのですが、この描画スレッドとJavaFX Application Threadを同期するイベントがPulseになります。

 JavaFXってマウスイベントとかにNodeを追随させようとしても、妙にマウスの動きからNodeの動きがずれるのが、改善されたりしないかとか思ったけど、全くそんなことはなかったぜ。

 これらPulse関連APIの使い道は、今のところ私には思いつきません。PulseListenerの方はレイアウトにどれぐらい時間かかってるか、デバッグ的な使い方くらいかなぁ。私の頭じゃ。



 Pulseについての詳細は以下のページを参照してください。
docs.oracle.com


Nested Event Loop

 さて、今回の記事の本題です。といっても、実はこのNested Event LoopはJavaFX初期からあって、3年前の私の記事でも実はついでに紹介しています。
nodamushi.hatenablog.com


 大ざっぱに書くと、JavaFX Application Threadは通常以下のような処理をしています。

 Nested Event LoopのAPIを使うと、この流れを以下の図のように、複製することが可能になります。

 複製と言っても、新しいスレッドが走っているとかではなくて、単純に呼び出した関数の中でイベント処理のループが走り始めると言うだけです。


 この機能がおもにどこで使われているかというと、showAndWaitメソッドです。showAndWaitでウィンドウが閉じるまで処理を待機しても、JavaFX Application Threadは停止することなく、他の処理を続けることが出来ます。この時に使われているのがNested Event Loopです。


 今までは、Stage.showAndWaitからしかアクセスできなかったこのNested Event LoopのAPIが公開され、いつでもどこでも使えるようになります。
 一番簡単に思いつく使い道で、かつ、効果が高いのは、Stageのダイアログと同様にNode単位でのモーダルダイアログでしょう。

f:id:nodamushi:20161211183827p:plain


 今までは、このようなことを実現しようとしたら、次のような2つに分かれた手順が必要でした。

  1. なんらかのイベントハンドラ等からダイアログ用のNodeを作り、対象ノードの操作を不可能にする。(ここで最初のイベントハンドラの処理終了)
  2. ダイアログのYesやCancelといったボタンが押されたときに動作するイベントハンドラが、残りの処理を行い、ダイアログを消去する。

f:id:nodamushi:20161211183828p:plain

 しかし、Nested Event Loopを使うと、一つのイベントハンドラの中で処理が完結します。

f:id:nodamushi:20161211183829p:plain


 これはかなりプログラムを書くのが楽ですね。
 最近はJavaもラムダを導入したり、実質finalが導入されたりと色々工夫されて、イベントハンドラを書くのが楽になってきましたが、それでも処理が二分割されるようなプログラムを書くのって面倒くさいんですよね。
 処理が二分割で済めば良いですが、何回かダイアログを表示して、しかもそのダイアログ内容が途中で分岐するような処理だったら、面倒なことこの上ないですよね。

 一つのEvent Handlerで処理が済むというのは、それだけで実装面からも保守管理の点からもメリットです。




Nested Event Loopの使い方

Nested Event Loopを利用するには、以下のフィールド変数(ローカル変数は駄目)が必要です。

private Object waitKey = null;
  • waitKey: Nested Event Loopを解除するのに必要になるキー。なくしたらexitできないので要注意。final宣言してしまっても良い。

 フィールド変数を用意したら、後は以下の関数を定義しておけば良いでしょう。
 なお、基本的にはJavaFX Application Threadからしか操作しないので、waitKeyをvolatile等にする必要はありません。

private Object waitKey = null;

public boolean isWait(){return waitKey!=null;}

// 戻り値はintなどに決まっているなら、それに変換しても良い。
protected Object enterWait(){
  if(!isWait()){
    waitKey = new Object();
    Object result = Platform.enterNestedEventLoop(waitKey);
    return result;
  }
  return null;
}
// 引数のresultは上↑のresultになる。
protected void exitWait(Object result){
  if(isWait()){
    Object key=waitKey;
    waitKey=null;
    Platform.exitNestedEventLoop(key,result);
  }
}

 そしたら、後はこんな感じ。

//ダイアログのNodeを作成完了後
cancelButton.setOnAction(e->exitWait(0));
okButton.setOnAction(e->exitWait(1));
noButton.setOnAction(e->exitWait(2));

//何かにダイアログを追加
targetPane.getChildren().add( dialog );

//ダイアログのボタンが押されるまで待機
boolean isOK = enterWait() == 1;

//ダイアログを削除
targetPane.getChildren().remove( dialog );

if(isOK){//OKなら処理
…
}

 むろん、Dialogクラスのように、ダイアログ表示機能と待機機能を何かのクラスにまとめておいても良いでしょう。



 このように、Nested Event Loopを使えば、処理の途中でGUIを介した処理の結果が必要である場面の記述が簡単になります。色々夢が広がリングですね。


 というわけで、今度こそ、良いお年を。

JavaFX9が良い感じになってきた件

 この記事はJavaFX Advent Calender 2016の9日目の@arachan@githubさんの記事になるそうです。
 前日は@skht777さんのJavaFXで動くプロ生ちゃんデスクトップマスコットを作る - Qiitaでした。
 明日は@Yucchi_jpさんのHitInfoを少しだけ…です。


 はい、テンプレってこんなもんで良いんですかね。
 何か今年やたらと参加者が少なくて盛り上がってる風がないですよね。寂しいですな。
 普段はこういうのは眺めてるだけで、関連記事とか書いてても参加しない奴なんですが、人が少なくて寂しいので、珍しく参加してみようかと。




今、JavaFX9が静かに熱い

 
 世にJavaFXが出てからというもの、派手に目立ったことはないように思います。

 私はJavaFX2からさわり初めて、本格的にJavaFX8から利用をしていますが、これまでの印象はおおよそこんな感じ。

  • JavaFX Script : そんな子はいなかった
  • JavaFX2 : え?WebView?あぁ、Qtですか?
  • JavaFX8 : そんな子がいるような気がする


 なんというか、なんだかんだちょっと凝ったことをしようとするとすぐに制限に引っかかったり、実装を追ってハックするような真似をしないといけなくて、正直に言えば、Swingの方が使いやすいと思っていました。

 しかし、そんなこれまでのバージョン達であっても、登場前や直後は話題にする人は結構話題に挙げていたのですが、JavaFX9の話題は全く盛り上がっていないようです。「JavaFX9」でググってみても、2016年12月現在、id:aoe-tkさんの記事ぐらいしかトップに出てきません。なお、JavaFX8ではいろんな記事が出てきます。

 たしかに、新しいコントローラなどが一切追加されない、非常に地味なアップデートしかありません。派手な話題は描きにくいかもしれません。


 だが、私は言いたい。


 JavaFX9はこれまでJavaFXにあった不満を多く解決してきたと。

 これまであってもなくてもぶっちゃけ変わらない、実に微妙な立ち位置でしかなかったあのJavaFXが。
 あのJavaFXがついにJavaFX9で、僅かに痒いところに手が届かないところまで来たと!



 そんなJavaFX9の良いところをちょっとだけ、紹介いたします。


Skinが公開された!

 皆さんもご存じ、ControlのSkinがついに、ついに、ついにjavafx.scene.control.skinパッケージとして公開されます。
 ほんと、ついにですよ。なんで最初から公開していないんですかね。


 本当にジミーな内容ですね。これが目玉の変更点なんだから、まー盛り上がりませんわな。(´・д・`)


 でも、今まではちょっと見た目を追加したいとか、ちょっと機能を追加したい等々、「ちょっとレールから外れたい」ということをやろうと思えば、com.sunパッケージをいやーな気分で操作するか、多大なコストを払って実装するか(それでも結局com.sumパッケージに行き着くんだけど)しかなかったんです。
 いや、もう、ホント、これで大手を振って色々実装できますよ。


 でも、今回公開されるのはSkinだけで、Behaviorは公開されません。BehaviorはControlの機能、動作を決定する重要なクラスです。com.sunパッケージ時代のSkinだと、コンストラクタの引数にBehaviorが要求されていたんですが、これらがなくなってしまいました。


 じゃぁ、Behaviorそのものがパージされてしまったのかというと、そうでもなくて、結局com.sunパッケージとして残っています。で、コンストラクタの引数ではなくて、コンストラクタの中で生成するようになってしまいました。うわぁ~ぃ(泣
 一応、privateなメソッドでgetBehaviorとかがあるので、そのうち公開する気なのかも知れませんね。気長に待っています。



 あ、そういえば、(少なくとも私が読んだ)JavaFX8までのBehaviorの動作って、次のようになっていたと思うんです。

  1. マウスイベントなどを受け取る
  2. マウスイベントなどからアクションに対応する「String」に変換する
  3. BehaviorにStringを投げる
  4. Stringに対応する処理を実行する


 全部が全部って訳ではないと思いますが、少なくともTextAreaのBehaviorは次のような感じに変わったようです。

  1. Behaviorがマウスイベントなどを受け取る
  2. イベントをキーとして、登録しているラムダを取得する
  3. ラムダを実行する


 見えないところも整理が進んでいるようですね。



Tooltipが良くなった!

 去年、私はこんな記事を書きました。
nodamushi.hatenablog.com

 要約すると、ざっと以下の二つが不満点。

  1. 表示の遅延、表示時間が変更できなくて不満だ
  2. フォーカス持っていないウィンドウでもツールチップが表示される上に、画面の最上にウィンドウが表示されてうざい。


 しかし、JavaFX9からは次の3つのPropertyが追加されます。

  • showDelay : 表示するまでの遅延時間。デフォルトは1秒
  • showDuration : 表示時間。デフォルトは5秒
  • hideDelay : マウスがNodeから離れてからTooltipが消えるまでの時間差。デフォルトは0.2秒
Label label = new Label("マウスを置いているとポップアップするよ。");
Tooltip tooltip = new Tooltip("すぐに現れて消えないTooltip!");
tooltip.setShowDuration(Duration.INDEFINITE);
tooltip.setShowDelay(Duration.ZERO);
label.setTooltip(tooltip);
Scene s = new Scene(label);
stage.setScene(s);
stage.show();

f:id:nodamushi:20161208203008p:plain

 いや~、ツールチップが消えないからキャプチャするのも急がなくて良いし楽だね。


 でも、残念なことに二つ目の不満点である、フォーカスを持たないウィンドウでもポップアップする挙げ句、ウィンドウを最上位まで持ち上げてきてしまう問題点は解決されていませんでした。



 これ結構な問題だと思うんだけど、中の人達に認識されていないのかな?



 あ、あと、これの解決方法が新しく追加されていないかと色々見てたときに気がつきましたが、JavaFX9からは現在表示しているJavaFXウィンドウの一覧をWindow.getWindows()で取得できるようになりました。これ、地味に嬉しい。
 私、これを自前で実装してたからね。

ObservableList<Window> windows = Window.getWindows()



子要素の順番を入れ替えなくても表示順序を変えられる!

 いままで、重なっている子要素の表示上下関係を入れ替えようと思ったら、getChildren()で子要素のリストを取得して、並びを入れ替えるしかありませんでした。
 Swingだったらzindexで変化するのに!と思っていたら、ついにJavaFX9でViewOrderプロパティが出来ました!


 ViewOrderは小さいほど手前に来ます。0が基準値です。負数も可能の様です。
 同じViewOrderの場合は子要素のリストに挿入された順番が反映される。

  Circle c1 = new Circle(100, 100, 100, Color.AQUA);
  Circle c2 = new Circle(100, 150, 100, Color.RED);
  Circle c3 = new Circle(300, 100, 100, Color.AQUA);
  Circle c4 = new Circle(300, 150, 100, Color.RED);
  pane.getChildren().addAll(
          c1,c2,//アクア色の円は赤より下
          c3,c4 //アクア色の円は赤より下
  );
  //c3,c4の表示順序を入れ替え、アクア色の円を赤の上に持ってくる
  c3.setViewOrder(0);//※0は初期値です
  c4.setViewOrder(1);

 実行結果の下図をご覧下さい。右側の二つの円はアクア色の円が赤色の円より上に表示され、左側の二つとは重なり方が変わっています。






念願の行番号付きテキストエディタが使える!

 JavaFX9で行番号付きのピュア(com.sunパッケージやリフレクションを利用しないという意味)なエディタが使えるようになります。
 主に私が作っていたアプリケーションの関係でどうしても欲しかったけど諦めてListViewとTextEditorを行き来することでなんとなくそれっぽい動作で諦めた、あの行番号付きエディタが!当時の私が知ったら号泣して喜んだでしょう。




 まぁ、これ、私が実装したんだけどね。ソースコードはGistにおいておきました。
JavaFX9でTextAreaに行番号がついたTextAreaを作ってみた · GitHub


 さて、先にも書いたように、ついにJavaFX9で今まで秘匿されてきたSkinの実装が公開されました。まだまだ公開して欲しいAPIはいっぱいあるのですが、重要なAPIは結構出そろってきました。その中の一つがText関連のAPIです。
 座標とTextの文字の位置を紐付けるHitInfoクラスTextInputControlSkinのgetCharacterBoundsメソッドが公開されたことにより、文字のインデックスとTextシェイプの座標内の対応が取れるようになりました。

 今回はこの二つを利用して行番号を表示すべき位置を取得し、TextAreaに行番号を付けてみました。


手順

 いい加減記事が長いので、詳細はソースコードを適当に眺めていただくとして、実装したものはザックリと説明すると以下の3手順です。

  1. まずは、TextAreaの文字列を行単位に分割する
  2. TextAreaの左上の座標と左下の座標の挿入位置を検出し、その範囲に入る行について3を処理する
  3. 各行の行頭文字のBoundsを取得し、その位置に会うように行番号を配置(空行などは上手いこと何とか処理する)


 まず、1の文字列を行単位に分割するのは別に説明は要らないでしょう。
 3の、表示位置の取得は、各行の行頭文字インデックスが分かれば、getCharacterBounds(index)で簡単に取得可能です。で、問題は2でした。


HitInfoの落とし穴

 さて、TextInputControlSkinから挿入位置を取得するには、getIndex(x,y)でHitInfoを取得するか、getInsertionPoint(x,y)で取得します。これらはどっちも結果は同じです。
 さて、画面左上の座標の挿入位置と、左下の座標の挿入位置を検出したいのだから、単純に考えるとこうなるはず。

int topInsert = getInsertionPoint(0,0);
int bottomInsert = getInsertionPoint(0,height);


 私は最初これで実装して、行番号が表示されなくて悩みました。

 これで取得されるtopInsertとbottomInsertは、topInsertは0で固定、bottomInsertは-1もしくは、画面の大きさ依存の値で飽和するかになります。

 と、言うのもここでの(0,0)や(0,height)の座標は、TextAreaの座標ではなく、あくまでText内での座標を指しています。従って、Textの原点位置での挿入場所は当然最初の0になってしまうし、(0,height)と指定しても、スクロールバーとか考慮されていないので、ある値で飽和してしまう。-1が返ってくるのは、(0,height)の範囲にTextシェイプが存在していなかったからかも知れないが、これに関しては詳しくはまだ追っていません。

 従って、目的のことがしたい場合は次のようにオフセットを考慮してあげる必要があります。

double offsetY = getCharacterBounds(0).getMinY();
int topInsert = getInsertionPoint(0,-offsetY);
int bottomInsert = getInsertionPoint(0,height-offsetY);
if(bottomInsert < 0)bottomInsert = textArea.getLength();

 最初の文字のY座標をオフセットとして引いてやると、上手くいく。
 bottomInsertは-1になる可能性があるから、-1になっていたら、ひとまずTextAreaの文字列長に設定してやれば良いと思われる。



 まぁ、これはHitInfoの落とし穴というよりは、getIndex(x,y)の仕様ミスだと思うけど。
 実際に公開するまでに変わったりするかもね?変えた方が良いと思うよ?


みんなJavaFX9使おうぜ!

 はい、というわけで、どうでしたでしょうか?
 これまでは、「はぁ~、これもできねぇのかよ」とか、「あ~、動きがもっさり」とか、「JavaでもQtでよくね?Swingでよくね?」「男なら黙ってCUI」などなど、散々(主に私に)捻られてきたJavaFXですが、かなり良い子になってきたと思います。

 Swingとは使い勝手がだいぶ違うので、今までSwingをやってたって人だと、それなりに学習コストを払わなくてはなりません。しかし、それを払うだけの価値はJavaFX9でそれなりに出てきたと思います。


 まだ画面がなーんかちらつくとか、3DShapeとかいらねーからOpenGL使わせろとか、シェーダー書かせろとか、KyeEventが使いにくいとか、Behaivor公開しろとか、言いたいことは山ほどありますが。まぁ、要求なんて何年たっても0になることはないので、焦ってもしょうがないですね。



 ぜひ、この機会にJavaFXを本格的に使いはじめてみませんか?ということで、今回の記事はおしまいです。

 最後まで読まれた方がいましたら、長々とおつきあいただきありがとうございました。





 実は、この記事は5日前に最後の行番号付きTextAreaの話だけで公開しようとしてたんです。でも、ついでにAdventカレンダーに登録してやろっかなーと思ったところ、明日のYucchi_jpさんと内容被りそうだったので、誤魔化すために色々追加したらこんなに長くなりましたとさ。も~、今年のブログ更新はこれで最後でいいな。良いお年を。

MarkdownビューワーをJavaFXで作ってみた

 Markdownを書くのに一番いい方法って何なんでしょうね?
 私はEclipse + GMF viewerを主に使っていたんですけど、このGMF viewerってディレクトリにhtmlファイル出力しちゃうのがすっごい気にくわないんだよねぇ。
 かといって、Firefoxとかでやるとしても、たとえKeySnailを入れてるとしても、EclipseとかEmacsのテキスト編集機能にはさすがにかなわないのよね。



 で、GitBucket作者のたけぞうさんがGitBucket用のMarkdownプロセッサ(markedj)をJava作って公開したという記事を見つけた。

 ほう、Javaとな。

 しかも、会社のGitサーバーに入れたのはGitBucketなので、私の利用環境との相性も良いじゃん。

 よし、markedjのビューワー作るか!JavaFXで!

というわけで、できた

以下からダウンロードできます。
Java8u40以上のJavaにちゃんとパス通ってればmarkedjviewer.jarをダブルクリックで起動するはず。
なお、Windows以外で動くかはわからんっ。前に作ったJarファイルの位置の検索がちょっとトリッキーなことしてるので、テストしたWindows以外で動くか謎。
markedjviewer


f:id:nodamushi:20151108202326p:plain

ソースコードはこちら
Git repository




使い方

 まぁ、ぶっちゃけほとんど機能なんてないので、説明するようなこともないんですが。。。

 起動するとこんな感じの画面です。
f:id:nodamushi:20151108204635p:plain

 上のバーのファイルボタンからMarkdownを開くなり、ドラッグアンドドロップをするなりで開くことができます。

 開いたら、………あとはスクロールするぐらいしか操作することはないですけどね。

 一応、現在の表示内容のHTMLを保存する機能(Ctrl+S)、画像等の変更を反映させるためのリロード(F5かCtrl+R)があります。
 メニューバーにあるコンフィグ(歯車のボタン)機能は張りぼてです。GUI作るのが面倒くさくなりました。




Emacsと組み合わせる

 このビューワーはMarkdownファイルを監視しているので、ファイル内容が変更されると、自動的に表示を更新してくれます
 本当は画像ファイルとかも監視したかったんだけど、WebViewが今何開いてるのかどうやって取得するのかわからなかった。まぁ、今後の課題と言うことで。

 さて、Emacsにはauto-save-buffers-enhanced.elという、ファイルを編集したら、自動的に保存を行ってくれる便利なプラグインがあります。


 ファイル更新監視 + 自動保存………もう、私が言いたいことはもうおわかりですね?
 つまり、Emacs+auto-save-buffers-enhanced.el+markedjviewerで結果を即座に確認しながら、書くことができるわけです!

 んほおおおおお、きもちいいいいのぉおおおおおお!!!

f:id:nodamushi:20151108202925p:plain

 あ、そうそう、表示領域を広くするために、ウィンドウがフォーカス持っていない場合はツールバーが消えるという地味な機能がついています。(上の画像ではビューワーにフォーカスがないので、黒いツールバーが表示されていない)




 というわけで、私にとっては結構便利なものがなんかできました。
 これで快適なMarkdown生活が送れます。



 

JavaFXでConsole作ってみた

 JavaFXってConsole的な物なくね?
 ググっても出てこなくね?


 と、いうわけで、作ってみました、こんなもの。
f:id:nodamushi:20150926211739p:plain


 System.outを今回作ったSimpleConsole.outに変更可能。
 マルチスレッド対応(たぶん)


 最低限の機能はあるんでない?誰かがしっかりした物を作ってくれるまではこれで我慢しましょうぞ。

NodamushiFXControls/SimpleConsole.java at master · nodamushi/NodamushiFXControls · GitHub

NodamushiFXControls/ConsoleTest.java at master · nodamushi/NodamushiFXControls · GitHub

JavaScriptで作ったちょっとしたスクリプトからファイルを保存したい

 JavaScriptはちょこっと何かを作ろうと思うと、一番使いやすいと思っている。なんせ特に何もいらない。エディタとブラウザがあればGUIを持った簡単なスクリプトなんかすぐ作れる。最近は簡単な物ならブラウザ内で全部完結して、エディタすらいらない。
 Excelマクロを習ってGUIの作成を習うよりずっと簡単だと私は思っている。なお、異論は認める。

 が、このJavaScriptは簡単ではあるが、ファイルの保存が出来ない。File APIもChromeしかない。
 私ならJavaScriptで何かして保存したいときはGreasemonkey使ったり、keysnail使ったりするって手段もあるっちゃある。
 でも、あまりプログラミングに詳しくない人が簡単に作って試して保存まで勢いで出来なければ意味が無い。


 だが、そんな方法はない。と、思っていたら意外とHTML5には別の手段で保存する方法があるようだ。
 以下のsaveTextを実行するだけで、とりあえずローカルに保存できる。ただし、ブラウザ設定によっては何も聞かずにDownloadフォルダに勝手に保存しちゃうのが玉にキズだけどね。

function saveText(text, fileName) {
  var a, blob, event;
  if (fileName == null) {
    fileName = "textfile.txt";
  }
  blob = new Blob([text], {
    type: "text/plain"
  });
  if (window.navigator.msSaveBlob != null) {
    window.navigator.msSaveBlob(blob, fileName);
  } else {
    a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.target = "_blank";
    a.download = fileName;
    event = document.createEvent("MouseEvents");
    event.initEvent("click", false, true);
    a.dispatchEvent(event);
  }
};


CoffeeScriptバージョン

saveText = (text,fileName)->
  if !fileName? then fileName = "textfile.txt"
  
  blob = new Blob [text],{type:"text/plain"}
  if window.navigator.msSaveBlob? then window.navigator.msSaveBlob blob,fileName
  else
    a = document.createElement "a"
    a.href = URL.createObjectURL blob
    a.target = "_blank"
    a.download = fileName
    event = document.createEvent "MouseEvents"
    event.initEvent "click",false,true
    a.dispatchEvent event
  return


参考:
JavaScriptだけでファイルの保存機能を実装する - 新人Webエンジニアの記録。
JavaScriptのクリックイベントを発火させる方法 - ゆうなんとかさんの雑記帳的な。yuuxxxx.hatenablog.com





ちょっと根性あるならnode-webkitという選択肢も

 去年ぐらいからちょくちょく耳にすることがあるnode-webkit。要するにHTML5とJavaScriptとそれの実行環境をひとまとめにしてアプリケーションにしましょうというものですかね。
 node.jsの機能が使えるのでファイルの保存とか出来ます

 詳しくはよく知らないけどね!(え 個人的には単体で動くブラウザのプラグインだと思っています。

 liginc.co.jp




まだ根性あるなら好みの関数を持つブラウザ作っちゃおうぜ

 回りくどいことせずに、JavaScriptからダイレクトに保存関数を呼べるブラウザを自作しちゃうってのも一つの手だ(え?


 例えば、Qtとか、JavaFXにはWebEngineがあるから、それを使うと、意外に簡単にJavaScriptで保存することが出来るブラウザを作れる。
 まぁ、確かに、最初のブラウザを作るのはちょい骨折れるけど、一回作ってしまったら、node-webkitの用に毎回アプリケーションにする必要なんて無いぞ!これは楽だ!




 ………いや、やっぱそれぐらいならJavaやらC#やらExcel VBAを覚えた方が絶対早いよ




 最後に載せたJavaFXのWebEngineを使った例に次のHTMLファイルを開かせると、ファイルの保存画面が開いてテキストファイルが保存される。





 さぁ、私の匙は投げられた。好きな方法を選ぶがよい。


test用HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script>
Files.saveText("内容");
</script>
</body>
</html>


自作ブラウザ

import java.io.*;
import java.nio.charset.Charset;
import java.nio.file.*;
import java.util.*;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.web.*;
import javafx.stage.*;
import javafx.stage.FileChooser.ExtensionFilter;
import netscape.javascript.JSObject;

public class Browser extends Application{

  public static void main(final String[] args){
    System.setProperty("prism.lcdtext", "false");
    launch(args);
  }
  private WebView view;
  private TextField url;

  @Override
  public void start(final Stage stage) throws Exception{
    final WebView v = view=new WebView();
    JSFiles.install(v);

    final TextField url = this.url = new TextField();
    final Button open = new Button("Open");
    url.setOnAction(this::openURL);
    open.setOnAction(this::openURL);

    final BorderPane p = new BorderPane(v);
    HBox.setHgrow(url, Priority.ALWAYS);
    p.setTop(new HBox(url,open));
    p.setPrefSize(800, 600);

    stage.setScene(new Scene(p));
    stage.show();
  }


  public void openURL(final ActionEvent e){
    final String text = url.getText().trim();
    if(text == null) {
      return;
    }

    String url;
    try{
      url = Paths.get(text).toUri().toURL().toString();
    }catch(final Exception e1){
      url = text;
    }


    final WebEngine engine = view.getEngine();
    if(url.equals(engine.getLocation())){
      engine.reload();
    }else{
      engine.load(url);
    }

  }



  public static class JSFiles{

    public static void install(final WebView v){
      final JSFiles jsf = new JSFiles(v);
      final WebEngine engine = v.getEngine();
      final JSObject window = (JSObject)engine.executeScript("window");
      window.setMember("JavaFiles", jsf);

      final JSObject eval = (JSObject)engine.executeScript(
          "var Files = {};"
          + "Files.saveText = function(str,charset){"
          + "JavaFiles.__saveText__(str,charset);"
          + "};");
      System.out.println(eval);
    }

    private WebView view;

    public JSFiles(final WebView view){
      this.view = view;
    }


    public boolean __saveText__(final String str,final String charset){
      final FileChooser chooser = new FileChooser();
      chooser.getExtensionFilters().addAll(TEXTFILTER,ALL);
      final Window w = view.getScene().getWindow();
      final File file = chooser.showSaveDialog(w);

      if(file!=null){

        Charset c =null;
        try{
          if(charset != null){
            c= Charset.forName(charset);
          }
        }catch(final Exception e){}
        if(c == null) {
          c = Charset.defaultCharset();
        }

        @SuppressWarnings("resource")
        final Scanner scan = new Scanner(str);


        final Iterator<CharSequence> i = new Iterator<CharSequence>(){
          @Override
          public CharSequence next(){
            return scan.nextLine();
          }
          @Override
          public boolean hasNext(){
            return scan.hasNextLine();
          }
        };

        try {
          Files.write(file.toPath(),()->i, c);
          return true;
        } catch (final IOException e) {
          e.printStackTrace();
          return false;
        }
      }else{
        return false;
      }
    }
    private static ExtensionFilter TEXTFILTER=new ExtensionFilter("*.txt", "*.txt");
    private static ExtensionFilter ALL=new ExtensionFilter("*", "*");
  }

}

ファイル選択のControl作った

 ウィーっす。
 さて、昨日は入力候補が出るTextFieldを作ってみました。nodamushi.hatenablog.com

 で、これを利用してファイル選択用のConrolを作ってみました。
 こんな感じに動作します。
f:id:nodamushi:20150814173654p:plain

 うん、使いやすい。私はやっぱりマウスでクリックするより、キーボードで打ち込んだ方が楽だからねー。
 昨日と同じくソースコードはこちら。NodamushiFXControls
 クラス名はnodamushi.jfx.path.PathChooserが今回のControlです。

 FileChooserとかDialogChooserとか自分で操作しなくても勝手にやってくれます。
 もともとこれを作りたいが為に、CompletionTextFieldを作りました。こう言うのって、ありそうで、検索してみても出てこなかったんだよね。
 (既にあったらすみません)






 しかし、まー、だいたい、目的の動作はするんだけど、まだ微妙にバグってたりはします。
 時々ListViewが上手くレンダリングされなかったり、ScrollBarが表示されているかどうかの判定にミスってるみたいなんだよねぇ。
 f:id:nodamushi:20150814175004p:plain
 f:id:nodamushi:20150814175006p:plain

 どっちも常に起こるわけじゃないんだよね。その発生条件もよく分かってない。
 直し方が分からんし、根性が切れたので、ここでいったん公開して、記事にしました。
 

入力候補が出るTextField作ってみた

 夏風邪ひいて熱が出たり咳が出たり鼻水出たりでしんどいですが、皆さん体調いかがでしょうか。


 さて、風邪ひいていようと暇なものは暇なので、タイトル通りの物を作っていました。
 イメージとしてはこんなのね。(Firefoxの検索窓)
f:id:nodamushi:20150813230113p:plain:w240

 ソースコードはこちら。NodamushiFXControls
 クラスは CompletionTextFieldです。
 候補や挿入方法はCandidateインターフェースで定義してあるだけなので、よく言えば自由がある、悪く言えばユーザーが用意しないといけない。

 デモ用に作ったCandidateインターフェースの実装(ForwardMatchCandidate)ではこんな感じに動作します。
f:id:nodamushi:20150813230150p:plain:w240
f:id:nodamushi:20150813230152p:plain:w240
f:id:nodamushi:20150813230154p:plain:w240





自作PopupControlの作り方

 意外とここの部分でつまった。
 最も単純な方法では、TooltipのGraphicに表示させたいNodeを突っ込んで表示するってのが楽そうだけど、やっぱそれだと微妙だよね。
 Tooltipの元となっているPopupWindowないし、PopupControlから派生させたい物です。


 今回はPopupControlにNodeを表示させる方法を調べたところ、単純にSkinを登録するだけでした。
 私が作ったコードではこんな感じ。ComboBoxSkinのコードを参考にしています。(ていうか、ほとんどここはまんまか)

    final PopupControl p = new PopupControl(){
      @Override
      public Styleable getStyleableParent(){
        return getSkinnable();//ポップアップ元のノードを返しています
      }
      {
        setSkin(new Skin<Skinnable>(){
          @Override public Skinnable getSkinnable(){return CompletionTextFieldSkin.this.getSkinnable();}
          @Override public Node getNode(){return getPopupContent();}
          @Override public void dispose(){}
          });
      }
    };





JavaFX8ではListViewの行の高さを取得できなかった

 ListViewを表示するときに、ある一定の行までは表示して、それ以上はスクロールバーで対応させたい。
 つまり、n行を表示するための高さを取得して、それをPrefHeightとすれば良いのだが、この1行の高さというのが実は取得できない。

 でも、ComboBoxとかではそれを実現しているように見える。ではどうやっているのかとソースコードを調べ、最終的に次のコードに落ち着いた。

  private static final Class<?> VIRTUALCONTAINERBASE;
  private static final Method
  GET_VIRTUAL_FLOW_PREFERRED_HEIGHT,//n行分の高さを取得するメソッド
  UPDATE_ROW_COUNT;//今何行分のアイテムがあるのかSkinに強制的に更新させる
  static{
    Class<?> c;
    Method m,m2;
    try {
      c=Class.forName("com.sun.javafx.scene.control.skin.VirtualContainerBase");
      m = c.getDeclaredMethod("getVirtualFlowPreferredHeight", int.class);
      m2 =c.getDeclaredMethod("updateRowCount");
      m.setAccessible(true);
      m2.setAccessible(true);
    } catch (final Exception e) {
      c = null;m=null;m2=null;
    }
    VIRTUALCONTAINERBASE=c;
    GET_VIRTUAL_FLOW_PREFERRED_HEIGHT=m;
    UPDATE_ROW_COUNT=m2;
  }


  private double getListViewPrefHeight() {
    double ph;
    final int maxRows = min(textField.getVisibleRowCount(),getCandidateSize());
    if (VIRTUALCONTAINERBASE!=null &&listView.getSkin()!=null &&
        VIRTUALCONTAINERBASE.isAssignableFrom(listView.getSkin().getClass())) {
        try{
          //これ↓を挟まないと次のgetVirtualFlowPrefreredHeightでちゃんと高さが出なかった。
          UPDATE_ROW_COUNT.invoke(listView.getSkin());
          ph =(double) GET_VIRTUAL_FLOW_PREFERRED_HEIGHT.invoke(listView.getSkin(), maxRows);
        }catch(final Exception e){
          final double ch = maxRows * 25;
          ph = Math.min(ch, 200);
        }
    } else {
      final double ch = maxRows * 25;
      ph = Math.min(ch, 200);
    }
    return ph;
  }


 う~ん、リフレクションかぁ。com.sunパッケージかぁ(´Д⊂ヽ

 とりあえず、これに期待するしかないっすね。。。いったいどの程度JavaFX9でPublic APIが増えるんでしょうかね。
 
JEP 253: Prepare JavaFX UI Controls & CSS APIs for Modularization



 特に落ちもまとめもありませんが、以上。