Brackets拡張機能を作った話1 パネルを追加・表示するまで

月花です。
Brackets拡張機能を作ってみたので、その話をします。

この記事で扱う範囲

最終的に搦め手になり、いろんな場所をいじったので記事が長くなるため、3つくらいに分ける。
この記事では、Brackets拡張機能を追加し、機能が搭載されていないパネルを表示するまでとする。
次の記事で機能の紹介をし、最後の記事で実際に拡張機能として搭載して完成するまでの話をする。

環境

High Sierraにはアップデートしてないんですけど、多分動くと思う。
ていうかアップデートに1時間以上かかるオフィスから持ち帰れないPC、どうやってアップデートしたらいいんですか?

どんな拡張機能を作ったか

f:id:gekka9:20171128013312p:plain
// なんか動画にしようと思ったけど、よくわからなかったので静止画です。伝わってください。

BracketsからiTunesを操作することで、コーディングをしながら1画面で快適なミュージックライフをサポートする。
音楽を全部詰めてぐちゃぐちゃなシャッフルをするタイプなので、ウィンドウ切り替えたりスマホいじったりで中断するのが嫌だったので。

手法と問題点

JXA ( JavaScript for Automation ) を使う。
developer.apple.com

これまで、スクリプトAutomatorからアプリを操作する場合はAppleScriptのみが対応していたが、JavaScriptでも書けるようになった。
ちょっと使ってみようかな、という動機しか無く、最終的にJavaScriptである必要はなくなった。

当初は、BracketsJavaScriptでできていて、かつアプリなので、横断してそのまま呼べるんじゃないかと思っていたが、そうはいかなかった。
というのも、Brackets自体がブラウザであるようで、エディタなどの機能はその上で動くフレームワークに過ぎなかった。
であれば、ブラウザからローカルのアプリが動かせるというのはセキュリティ的に危険極まりないのでできるわけがなかった。
そのため、搦め手で実装することにした。

Brackets自体からでは無理だが、権限のあるユーザが、適切なスクリプトを動かせばよい。
具体的には、mac標準のapacheでローカルサーバを建て、その中のPHPからJXAで書いたjsファイルを実行する。
BracketsからこのPHPへは、ajaxによる非同期通信で行う。
この流れであれば、サーバ上のバッチが動くのと同義になるため、動かせるはずだ。というか、動かすことができた。

拡張機能の作り方

基本的にはBracketsAPIのページを参考に組み立てていく。
Brackets API

BracketsJavaScriptフレームワークなので、言語は全てJavaScriptとなり、画面上の全てはHTMLである。
デバッグ機能は、メニューの デバッグ > 開発者ツールを表示 で立ち上がる。
どうもChromiumで動いているようで、Chromeの開発者ツールとほぼ同じものが立ち上がった。
ということは console.log() などが呼べるので、変数ダンプもらくちんで非常に助かる。

さて、実際に作っていこう。
拡張機能の雛形のようなものを探してみたが見当たらなかったので、既存の拡張機能を拝借して、中身を書き換えることで実装するのが楽だと思う。
ドキュメントとにらめっこして、作りたい拡張機能に必要なモジュールを追加したり、逆に削除していけばいい。

ファイル構成はこんな感じになる。

extensions/
    user/
        html/
            assets/
                css/
                    iTunesController.css
            panel.html
        main.js
        package.json

このうち、必須なのは main.js と package.jsonで、これらはそれぞれ、拡張機能のメイン関数と情報定義を行う。
公開しないのであれば、package.jsonは適当でいいんじゃないかと思う。
公開するなら、説明文とかライセンスはキッチリ書こう。この内容が拡張機能検索画面に載ると思う。

main.js に書くべきこと

ここでは、紹介した画像のような、エディタの下にパネルを追加する形式の拡張機能を作る際に、どのような手順で何を書いていくかを説明する。
ざっくりと次のようなフェーズに分ける。

  • 依存モジュール読み込みフェーズ
  • 設定読み込み・登録フェーズ
  • コマンド設定・登録フェーズ
  • テンプレート読み込みフェーズ
  • 必須関数定義フェーズ
  • ユーザ関数定義フェーズ

これらはすべて、main.js 内の define() 内で行う。

define( function( require, exports, module ) {
    'use strict';
});

すなわち、このdefine() が、拡張機能のメイン関数となる。
use strict とあるが、厳格モードが必要かどうかはよくわかっていないが、よほど汚いコードを書かざるを得ない場合でもない限り、厳格モードでもさほど問題はないだろう。いつものようにキレイに書こうな!
なお、これ移行のサンプルコードはすべて define() 内に収まっていると思ってほしい。

依存モジュール読み込みフェーズ

まずは、これ移行の処理を行っていくために、Bracketsフレームワーク(以下このように呼称するが、正式な名称ではない)の必要なモジュールのインスタンスを確保する。

    var Menus = brackets.getModule( 'command/Menus' ),
        CommandManager = brackets.getModule( 'command/CommandManager' ),
        PreferencesManager = brackets.getModule( 'preferences/PreferencesManager' ),
        WorkspaceManager = brackets.getModule( 'view/WorkspaceManager' ),
        Resizer = brackets.getModule( 'utils/Resizer' ),
        AppInit = brackets.getModule( 'utils/AppInit' ),
        ExtensionUtils = brackets.getModule( 'utils/ExtensionUtils' );

Brackets API
こんな感じで、API辞書とにらめっこして、必要なものを読み込んでいこう。
大体名前から何をするものかはわかるので、必要そうな単語で検索して、たとえばパネルの追加なら、WorkspaceManager のページを開いて、 add とか create とかでさらに絞り込んでいけば、パネルの追加に必要な関数がわかる、という具合だ。
そのページには、そのモジュールが依存しているモジュールも記載されているので、それも読み込む必要があるが、なくても動いたりする。

テスト実行時に管理者ツールを起動しておくと、足りなかったときはエラーが出るので、それで潰していってもよい。

設定読み込み・登録フェーズ

たとえば、パネルを開きっぱなしにして閉じて、再度開いた時に、この設定に状態を書き出しておけば、以前の状態に復帰できる。

    var preferences = PreferencesManager.getExtensionPrefs( 'iTunesController' );
    preferences.definePreference( 'enabled', 'boolean', false );

必要な数だけ登録しておくとよい。
この拡張機能の場合は、開いているか閉じているか、だけとなった。
実際にパネルを開く際に、定義した設定に書き込んだり読み込んだりするので、今はどんな名前の設定を保持したいか定義するだけ。

コマンド設定・登録フェーズ

ユーザがメニューから何か選んで実行する際には、コマンドという形式で呼ばれてくる。
なので、今回はパネルを開いたり閉じたりするトグルコマンドを定義する。

    var COMMAND_ID = 'iTunesController.enable';
    CommandManager.register('iTunesController' , COMMAND_ID, togglePanel );

CommandManager.register() の第三引数は、メニューが押下された際に実行する関数で、無名関数としてもいいと思う。
関数として定義する場合は、define() の中で行おう。

次に、定義したコマンドをメニューに追加する。
このアプリでは、Macでいうところの上部にあるあのテキストメニューと、エディタ画面の右端にあるアイコンメニューに登録する。

    var menu = Menus.getMenu( Menus.AppMenuBar.VIEW_MENU ),
        contextMenu = Menus.getContextMenu(Menus.ContextMenuIds.PROJECT_MENU);
    if ( menu !== undefined ) {
        menu.addMenuDivider();
        menu.addMenuItem( COMMAND_ID, '' );
    }
    if ( contextMenu !== undefined ) {
        contextMenu.addMenuDivider();
        contextMenu.addMenuItem( COMMAND_ID );
    }

これで、メニュー上に iTunesController という名前のトグルボタンが追加された。

テンプレート読み込みフェーズ

パネルやアイコンのデザインを定義するテンプレートやCSSを読み込む。
htmlファイルにしてもいいし、DOMを作って叩き込んでもよい。

    var controllerPanelTemplate = require( 'text!html/panel.html' ),
        controllerPanel,
        projectUrl,
        controllerIcon = $( '<a href="#" title="' +EXTENSION_NAME + '" id="itunes-controller-icon"></a>' );
    // Load stylesheet.
    ExtensionUtils.loadStyleSheet( module, 'html/assets/css/iTunesController.css' );

これはあくまでもテンプレートのため、動的に動く部分はJavaScriptでDOM操作を行い、表示していく。
なので、ボタン類以外は空の、構造だけのテンプレートでよい。
もしくは、テンプレートエンジン Mustache を標準で扱えるようなので、大規模になる場合はそれで実装するのがいいだろう。

<div id="itunes-controller" class="itunes-controller bottom-panel vert-resizable top-resizer todo-file">
    <div class="toolbar simple-toolbar-layout">
        <div class="title">iTunes Controller</div>
        <div id="itunes-controller-playing">
            <i class="fa fa-folder-open-o" aria-hidden="true"></i> <span id="album"></span> / 
            <i class="fa fa-user" aria-hidden="true"></i> <span id="artist"></span> / 
            <i class="fa fa-music" aria-hidden="true"></i> <span id="name"></span>
        </div>
        <a href="#" class="close">&times;</a>
    </div>
    
    <div class="table-container resizable-content">
      <div id="itunes-controller-controller">
        <a id="itunes-controller-backward"><i class="fa fa-backward" aria-hidden="true"></i></a>
        <a id="itunes-controller-play"><i class="fa fa-play" aria-hidden="true"></i></a>
        <a id="itunes-controller-pause"><i class="fa fa-pause" aria-hidden="true"></i></a>
        <a id="itunes-controller-forward"><i class="fa fa-forward" aria-hidden="true"></i></a>
      </div>
    </div>
</div>

この拡張機能では、こんな感じのテンプレートになった。
画像を作るのがめんどくさいので、FontAwesome がしれっとでてきているけど、本筋ではないのでファイルツリーとかサンプルコードとしては記載しない。
fonts ディレクトリ作って、CSS追加で読み込むだけ。

必須関数定義フェーズ

前項までで、ようやく定義が終わり、ここからは実際にビジネスロジックを書いていく。
このフェーズでは、今回の拡張機能に必須な関数を定義していく。
今回は1つだけ、Brackets が ready になった際に呼ばれる関数を実装する。

    AppInit.appReady( function() {
        var panelHTML = Mustache.render( controllerPanelTemplate, {} );

        WorkspaceManager.createBottomPanel( 'itunesController.panel', $( panelHTML ), 50 );
        controllerPanel = $( '#itunes-controller' );
        controllerPanel
            .on( 'click', '.close', function() {
                enablePanel( false );
            } );

        setNowPlaying();
        setInterval(function(){
            setNowPlaying();
        },5000);
        $('#itunes-controller-backward').on('click',function(){
            controll('previous');
            return false;
        });
        $('#itunes-controller-play').on('click',function(){
            controll('play');
            return false;
        });
        $('#itunes-controller-pause').on('click',function(){
            controll('pause');
            return false;
        });
        $('#itunes-controller-forward').on('click',function(){
            controll('next');
            return false;
        });

        controllerIcon.click( function() {
            CommandManager.execute( COMMAND_ID );
        } ).appendTo( '#main-toolbar .buttons' );

        if ( preferences.get( 'enabled' ) ) {
            enablePanel( true );
        }
      
    } );

まずはパネルのレンダーと、パネルの create 、そして各種リスナーの定義を行っている。
setNowPlaying() や controll() はユーザ関数で、詳しくは次の記事で扱うが、ここでは基本的にリスナーの定義・登録に留めるとスッキリすると思う。
setNowPlaying() は現在再生中の曲を取得する関数で、初回実行による初期化と5秒ごとのタイマー実行の定義を行っており、定義の範疇からは抜け出していない・・・と思っている。
その後、アイコンメニューへのリスナー追加と、設定ファイルから前回の情報を読み出して、パネルを開いておく、ということをやっている。

ユーザ関数定義フェーズ

前項でいくつか呼び出していたような、独自関数の定義を行う。
ビジネスロジックは基本的にここで書くべきだと思う。
今回は下記のような独自関数を実装した。

  • enablePanel() : 実際のパネル表示の切り替え
  • togglePanel() : 現在の開閉状況に従い、 enablePanel() を呼び出す
  • setNowPlaying() : 現在再生中の曲を取得し、パネルへ記載する
  • controll(string) : 再生や一時停止、前後の曲への移動などの操作を行う

iTunes 操作関連は JXA が絡み、ややこしいので次の記事で詳しく話す。

まとめ

ここまでで、ビジネスロジックを抜きにして、ユーザ関数の定義やリスナー追加もふっ飛ばせば、とりあえずパネルが開くだけの拡張機能ができているはずだ。
あとはガリガリビジネスロジックを載せていくだけとなる。