ぽよメモ

レガシーシステム考古学専攻

AngularJSでパスにスペースを含むimgをbackground-imageに指定する

例えば/hoge/fuga/contain space image.jpgみたいなjpegファイルを表示したいとき,ng-srcに値を渡せば問題ありません.
しかし,これをbackground-imageに指定したいとき,つまりAngularJSでCSSを使って画像を表示したいとき,ディレクティブを自作するのが一番良い,というのがいくつかstackoverflowのページを何個か見て得た答えです.

いくつかの例: stackoverflow.com stackoverflow.com

しかしこれは全てスペースを含んだパスではうまく動作しません.結果として値を渡された後,

background-image: url(/hoge/fuga/contain space image.jpg);

となり,スペース部分でエラーを吐くからです.

というわけで以下のようなディレクティブを作成しました.

var app = angular.module("myApp", []);

app.directive('backImg', function(){
    return function(scope, element, attrs){
        attrs.$observe('backImg', function(value) {
            element.css({
                'background-image': 'url("' + value +'")'
            });
        });
    };
});

普通に考えてみれば簡単な話だったのですが,要するにurl()にファイルパスを文字列として渡せれば良いわけで,このようにくくってやればいいわけです.
使い方は以下.

<ons-template id="sample.html">
<ons-page ng-controller="SampleController">
  <ons-toolbar>
    <div class="center">{{ data.title }}</div>
  </ons-toolbar>
    <div back-img="{{data.path}}"></div>
</ons-page>
</ons-template>
app.controller("SampleController", ["$scope", function($scope){
  $scope.data = {
    title: "SampleImage",
    path: "/path/to/contain space image.jpg"
  }
}]);

これで半日潰したあの日が懐かしいです.

monacaでCordova Fileプラグインを使う

ちょっとしたハッカソンAndroidアプリケーションを作成する際,ローカルのファイルをちょっといじれて,APIをちょっと叩けて,くらいの能力があれば良いと言うことで,monacaを利用したハイブリッドアプリケーションを作りました.

その際,CordovaFileプラグインを使うことを考えついたのですが,web上のリファレンスを見ても,TEMPORARYな部分や,PERSISTENTであっても自分のアプリケーションに割り当てられる領域のようなものばかりを扱っていて,いわゆる/mnt/sdcard/0/直下みたいなところをどう覗いたら良いんだろうとかなり悩みました.

今回はmonacaにおいて,OnsenUI 2.x,AngularJS 1.xを用いることとします.AngularJSの記法に則って書いていきます.

権限の設定

まず,Fileプラグインを導入しただけではダメです.config.xmlの適当な箇所に

<preference name="AndroidPersistentFileLocation" value="Compatibility"/>

これを追記しておきます.これが無ければ権限ではじかれます.
Content-Security-Policyは以下のように設定していました.

<meta http-equiv="Content-Security-Policy" content="default-src * data:; style-src * 'unsafe-inline'; script-src * 'unsafe-inline' 'unsafe-eval'">

PERSISTENTのルートを取得

ファイルシステムのルートディレクトリを取得するため,factoryを追加します.

let app = angular.module("myApp", ["onsen"]);

app.factory("FileSystem", ["$q", function($q){
    return {
        getFileSystem: function(){
            let deferred = $q.defer();
            window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, function(filesystem){
                deferred.resolve(filesystem);
            }, function(error){
                deferred.reject(error);
            });
            return deferred.promise;
        }
    };
}]);

$qは非同期処理のためのAngularJSのライブラリです.詳しくはググってください.
今後,追加していくサービスはできる限り非同期で書いていきます.

適当なcontrollerを作成し,先ほどのメソッドを読んでみます.

<ons-template ng-controller="SampleController">
  <ons-page>
    <ons-toolbar>
      <div class="center">Test</div>
    </ons-toolbar>
    <h1>てすと</h1>
    <span>{{ROOT_PATH}}</span>
  </ons-page>
</ons-template>
app.controller("SampleController", ["$scope", "FileSystem", function($scope, FileSystem){
  FileSystem.getFileSystem()
  .then(function(fs){
    $scope.ROOT_PATH = fs.root.toURL(); /* ルートへのパスを取得 */
  }, 
  function(error){
    alert("エラーです");
  });
}]);

これを動かしてダメだったら僕には何も言えませんお手上げです.頑張ってリファレンスを読んでください…

特定のディレクトリを取得

ディレクトリ名を指定して,先ほどのルート以下にあるディレクトリを1つ取得します.
ここでは指定しませんが後でMusicを引数として渡し,Musicディレクトリを取得します.中に何か入っているディレクトリにすると今後がやりやすいです.

app.factory("DirectoryMethod", ["$q", "FileSystem", function($q, FS) {
    return {
        getTargetDirectory: function(dirPath) {
            let deferred = $q.defer();
            FS.getFileSystem()
            .then(function(filesystem){
                filesystem.root.getDirectory(dirPath, {create: false}, function(dirEntry){
                    deferred.resolve(dirEntry);
                },
                function(erroe){
                    deferred.reject(error);
                });
            });
            return deferred.promise;
        }
    };
}]);

ルートの取得まで含めましたが分離したい人はその部分だけ削ってしまえば良いと思います.
もし読み取ろうとするディレクトリが存在しない場合,{create: true}を指定すると勝手に作成してくれます.

このときgetTargetDirectoryメソッドの成功時の返り値はDirectoryEntryオブジェクトです.これに対しcreateReader()してReaderを作成します.

ディレクトリ一覧を返す

先ほどのメソッドにチェーンさせて中身の一覧を返すメソッドを作ります.

/* DirectoryMethodに追記 */
        returnDirList: function(Entry) {
            let deferred = $q.defer();
            Entry.createReader().readEntries(function(entries){
                    deferred.resolve(entries);
                }, 
                function(error){
                    deferred.reject(error);
                }
            );
            return deferred.promise;
        }

このメソッドの返り値はDirectoryEntryまたはFileEntryのリストになります.readEntries()以下でfor文回してentries[i].isDirectoryとかで判定してディレクトリのみを抜き出せばディレクトリのみを返させることが出来ます.
ファイルのみを抜き出したい場合は.isFileを使えば良いです.

ここまででディレクトリとその中身を見るファイラーモドキを作れます.

ディレクトリを閲覧する何かを作る

<ons-template id="sample_main.html">
  <ons-navigator var="SampleNavi">
    <ons-page ng-controller="SampleController">
      <ons-toolbar>
        <div class="center">
          ファイラーっぽい何か
        </div>
      </ons-toolbar>
      <ons-list ng-show="dir_load" class="animated">
        <ons-list-header>Directory</ons-list-header>
        <ons-list-item ng-repeat="dir in dirs | orderBy: 'name'" ng-click="load(dir)" tappable>
          <div class="left"><ons-icon size="20px" icon="md-folder-outline" class="list__item__icon"></div>
          <div class="center">{{::dir.name}}</div>
        </ons-list-item>
      </ons-list>
    </ons-page>
  </ons-navigator>
</ons-template>

<ons-template id="sample_child.html">
    <ons-page ng-controller="SampleController">
      <ons-toolbar>
          <div class="left"><ons-back-button>Back</ons-back-button></div>
          <div class="center">{{ SampleNavi.topPage.data.name }}</div>
      </ons-toolbar>
      <ons-list ng-show="dir_load" class="animated">
        <ons-list-header>{{SampleNavi.topPage.data.name}}</ons-list-header>
        <ons-list-item ng-repeat="dir in dirs | orderBy: 'name'" ng-click="load(dir)" tappable>
          <div class="left">
            <ons-icon size="20px" icon="md-folder-outline" class="list__item__icon" ng-if="dir.isDirectory"></ons-icon>
            <ons-icon size="20px" icon="md-file" class="list__item__icon" ng-if="!dir.isDirectory"></ons-icon>
          </div>
          <div class="center">{{::dir.name}}</div>
        </ons-list-item>
      </ons-list>
    </ons-page>
</ons-template>
app.controller("SampleController", ["$scope", "DirectoryMethod", function($scope, DirMethod){
  let abort_error = function(error){
    alert("エラーだよ");
  }
  DirMethod.getTargetDirectory("Music")
  .then(DirMethod.returnDirList)
  .then(function(dirList){
    $scope.dirs = dirList;
    $timeout(function(){
      $scope.dir_load = true;
    });
  }).catch(abort_error);
  $scope.dir_load = false;
  $scope.load = function(dirEntry) {
    let options = {data: dirEntry, animation: "slide"};
    DirNavi.pushPage("sample_child.html", options);
  };
}]);

こんな感じのメソッドを利用して以下のような感じのものにしてみました.
f:id:pudding_info:20161105013731p:plain

これでファイルをタップして開こうとするとエラーを吐きます.例えば画像を表示したいとかなら,<img ng-src="{{hoge.path}}">とかで画像ファイルへのパスを渡してやれば表示できます.
簡単なとこまでしか書いてませんが疲れました.そのうち気が向いたら上記のファイラーモドキとMediaプラグインを組み合わせて簡易音楽プレーヤーを作る記事を書きます.

Karabiner-Elementsのプロファイルを動的に切り替え

2016/11/8現在,最新バージョンは0.90.64となり,デフォルトでプロファイル切り替え機能が実装されました.詳しくは以下に.

poyo.hatenablog.jp

きっかけ

Macの環境をmacOS Sierraのリリースと同時に新規インストールでまっさらにしました.
後期から研究室に居場所ができるので,手元に余っているMajestouch黒軸を置いて使おうかなと.そこでKarabinerをインストールしようとしたら

Karabiner - OS X用のソフトウェア より

macOS Sierraサポート状況

Karabinerは今のところmacOS Sierraでは動作しません。

Sierra対応は、まずは単純なキーの変更を行える機能をKarabiner-Elementsとして開発中です。 (設定画面を除くとSierra上で動作しています)

Karabinerのフル機能のSierra対応はKarabiner-Elementsが完成してから対応します。

https://github.com/tekezo/Karabiner-Elements

非常につらい…しかしkarabiner-elementsというソフトウェアが開発中であり,キーの変更は可能であることが分かりました.インストールし,手動で設定をしたところ確かにきちんと設定はできたのですが,複数のプロファイル切り替えを手動でする必要があり,面倒だなーと.

そこでPythonでUSB機器を監視し,特定のデバイスが接続されたときにKarabiner-Elementsの設定ファイルであるkarabiner.jsonを書き換えるようなスクリプトを書き,launchdを使ってデーモンとして登録することで動的な書き換えができるようになりました.

karabiner.jsonを設定

karabiner.jsonの基本的な設定は以下です.

{
    "profiles": [
        {
            "name": "Default",
            "selected": true,
            "simple_modifications": {
                "caps_lock": "japanese_kana",
                "right_shift":"japanese_eisuu"
            }
        }
    ]
}

キー割り当ては以下のようにします.

"simple_modifications": {
    "キーボード上のキー": "割り当てるキー",
    "right_shift":"japanese_eisuu"
}

試しにJIS配列のキーボードにおけるCapsLockをControlに,変換・無変換キーをそれぞれJIS配列Macにおける英数とかなに割り当てます.

{
    "profiles": [
        {
            "name": "Default",
            "selected": true,
            "simple_modifications": {
                "caps_lock": "left_control",
        "japanese_pc_nfer": "japanese_eisuu",
        "japanese_pc_xfer": "japanese_kana"
            }
        }
    ]
}

その他のキーの名称は以下のソースコード中にあります. https://github.com/tekezo/Karabiner-Elements/blob/master/src/share/types.hppk

複数のプロファイルを設定

簡単なことでprofiles以下に複数設定するだけです.

{
    "profiles": [
        {
            "name": "Default",
            "selected": true,
            "simple_modifications": {
                "caps_lock": "left_control"
            }
        },
        {
            "name": "Second",
            "selected": false,
            "simple_modifications": {
                "caps_lock": "left_control",
        "japanese_pc_nfer": "japanese_eisuu",
        "japanese_pc_xfer": "japanese_kana"
            }
        }
    ]
}

ここで,selectedという項目をtrueにするとそのプロファイルが有効になります.

karabiner_profile_changer

そこでこういうものをつくりました.

github.com

これは一定時間おきに接続されたUSBデバイスを監視し,外付けキーボードが接続されたときに特定のプロファイルを有効にします.
バイスやそれに紐付けるプロファイル名はcloneしたディレクトリ内にconfig.jsonを作製し,そこで指定することができます.また,監視する間隔およびkarabiner.jsonの場所を指定します.

実行する前準備として,lsusbおよびPythonのパッケージとしてPyUSBをインストールします.Macにはhomebrewがインストールされていることとします.

$ brew install lsusb
$ pip install --pre pyusb
$ git clone https://github.com/pddg/karabiner_profile_changer.git
$ cd karabiner_profile_changer
$ cp config.json.sample config.json
$ cp karabiner_profile_changer.plist.sample karabiner_profile_changer.plist

バイスを指定

config.jsonへの記述は以下のようにします.

{
    "default_profile": "Default",
    "config_file": "/path/to/karabiner.json",
    "interval": 10,
    "devices":[
        {
            "profile_name": "Device1",
            "vendor_id": "0x4d9",
            "product_id": "0x2011"
        }
    ]
}

このとき,karabiner.jsonにはDefaultDevice1というプロファイルが存在することとします.何も刺していない状態でmain.pyを動かすと,Defaultプロファイルのselectedtrueになります.
バイスの指定にはvendor_idproduct_idが必要になります.この値は16進数で記述する必要があります.

これら二つの値は幸いなことにkarabiner-elementsで簡単に知ることができます.
karabiner-elementsのlogというタブを開き,外付けキーボードを抜き差しすると以下のようなログが出ます.

f:id:pudding_info:20160926184203p:plain

このうちのひとつに着目すると,以下のようにvendor_idproduct_idを知ることができます.

f:id:pudding_info:20160926184207p:plain

これをconfig.jsonに記述すると,このデバイスとプロファイル名を結びつけることができます.もちろん複数のデバイスを登録し切り替えることができますが,現状USBデバイスのみしか対応はしていないため,Bluetoothキーボードでは使えません.

launchdに登録

launchdはinit等と同じデーモン管理をしています.リポジトリに同梱されている.plistファイルに,main.pyへのパス,pythonインタプリタへのパス,そしてユーザ名を記述し,保存します.
登録は以下のようにします.

$ cp karabiner_profile_changer.plist ~/Library/LaunchAgents/
$ launchctl load ~/Library/LaunchAgents/karabiner_profile_changer.plist

一度loadすると,次回からはOSの起動時に自動で起動します.
plistファイルを変更した場合はunloadしてからもう一度loadする必要があります.

まとめ

このスクリプトはあくまで記述したkarabiner.jsonに記述したプロファイルのselectedを書き換えるだけであり,それ以外の要素は改変しません.プロファイル名はわかりやすいようにデバイス名などにしておくことをオススメします.
短時間で書いたスクリプトですのでバグ等が存在する可能性があります.お気づきになられた場合はプルリクエスト等でお知らせください.