Electron+Macで「このアプリケーションで開く」に対応する

# TL;DR
`electron-builder` を使用しているElectronアプリをmacOSの「このアプリケーションで開く」に対応させます。  
さらに、アプリ内でファイルパスを受け取る方法も取り上げます。

<img width="683" alt="4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74419/0a18fd4a-9137-ff2c-5df0-c2af074f6e07.png">

# 行儀のいいエディターの挙動
特定の形式のファイルを編集するためのエディターをElectronで作る時など、特定のファイル形式と自作アプリケーションを関連づけたい時があると思います。

Macで動作する他のエディターの代表としてVisual Studio Codeを観察してみました。
以下のような挙動があることがわかります。

## 「このアプリケーションで開く」に表示される
<img width="630" alt="1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74419/d6961000-3fa7-39e1-5c69-462b11b62865.png">
VSCodeはJSONファイルの編集に対応しているので、FinderにてJSONファイルを右クリックすると、「このアプリケーションで開く」の一覧に現れます。

## 「推奨アプリケーション」で明るく表示される
<img width="537" alt="2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74419/21c83bdb-38af-5faa-db44-99623b49d355.png">
「このアプリケーションで開く」メニューにて「その他…」を選ぶと、Macにインストールされているアプリケーション一覧が表示されます。選択対象を「推奨アプリケーション」にした場合、そのファイルに対応していないアプリケーションがグレーアウトします。VSCodeはJSONファイルの編集に対応しているので、VSCodeが明るく表示されています。

## ファイルにアイコンが設定されている
JSONファイルに専用のアイコンが設定されています。
(余談ですが、上の画像にてJSONファイルに設定されているアイコンはVSCodeのものではなく、Adobe製品によるものだと思われます。)

## ダブルクリックすると開く
当たり前ですが、ファイルアイコンをダブルクリックしたり、「このアプリケーションで開く」にてVSCodeを選択すると、VSCodeでそのファイルが開かれます。

# これらの挙動はInfo.plistによるもの

世の多くのエディターが上にあげたものと同様の挙動をするにもかかわらず、あなたの作ったエディターは「このアプリケーションで開く」に現れませんし、「推奨アプリケーション」にてグレーアウトされると思います。

それは、`Info.plist` を正しく設定していないからです。

試しにVisual Studio CodeのInfo.plistを覗いてみましょう。
Finderからアプリケーション→Visual Studio Codeを右クリック→パッケージの内容を表示 です。

<img width="646" alt="3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74419/950db82f-70cd-cba8-59f5-b6f848023854.png">

`plist` はプロパティリストの略だそうで、いろんな値が設定されています。
その中に`Document types`という項目があり、しっかりとJSONファイルも定義されています。

このDocument typesを適切に設定してあげることで、自作のアプリケーションも「このアプリケーションで開く」に対応させ、「推奨アプリケーション」に表示させることができます。

# ビルド時にInfo.plistに項目を追記する

electron-builderには、このInfo.plistに項目を追記する機能が備わっています。

package.jsonに以下のように `extendInfo` の項目を追加します。

``` JSON:package.json(一部省略)
{
  "build": {
    "productName": "My Awesome Application",
    "appId": "com.wararyo.myawesomeapplication",
    "mac": {
      "icon": "build/icons/icon.icns",
      "extendInfo": {
        "CFBundleDocumentTypes":[
          {
            "CFBundleTypeExtensions": ["myextention"],
            "CFBundleTypeIconFile": "My Awesome Application.icns"
          }
        ]
      }
    }
  }
}
```

この例では、このアプリケーションが拡張子`.myextention`のファイルに対応していると認識されるように記述します。ついでにアプリアイコンを割り当てています。

あとはいつものようにbuild、dmgファイルを開きインストールすれば、「このアプリケーションで開く」に現れています。

<img width="683" alt="4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74419/0a18fd4a-9137-ff2c-5df0-c2af074f6e07.png">

ちなみに私は`.eclairconversation`という拡張子を編集するためのアプリケーション `Eclair Conversation Editor`を作成しました。

# 第2引数にファイルパスは渡らない

Windowsにてファイルを開くときはファイルパスが第2引数に渡るので、以下のようなコードでパスを取得することができます。

```index.js
//引数でファイル指定があったらそれを開く
if(process.argv.length > 1) {
  var p = process.argv[1];
  if(p.match(/\.[a-zA-Z]+$/)) load(p);
}
```

この方法はMacでは使えません。
Mac版のElectronにおいては起動時に`open-file`というイベントが`app`に対して渡るので、そこでファイルパスを取得します。
ファイルを開くとともにアプリケーションが起動する場合の他に、すでに起動しているアプリケーションでファイルを開く場合にもこのイベントが発行されます。

```index.js
import { app } from 'electron'
app.on('open-file', (event,path) => {
  event.preventDefault();
  load(path);
});
```

ただし、ファイルを開くとともにアプリケーションが起動する場合、`open-file`が発行されるタイミングではまだmainWindowが立ち上がっていないと思います。
そのときは一旦別の変数に退避させ、また別のタイミングでファイルを開く処理を行います。

これに関してはアプリケーションによって実装が異なってくるかと思います。参考に私のアプリケーションでの実装を貼っておきます。`electron-vue`を使用しています。

```index.js
var mainWindow;

app.on('open-file', (event,path) => {
  event.preventDefault();
  if(mainWindow === void 0) process.openFile = path;
  else mainWindow.webContents.send("Load",path);
});

app.on('ready', () => {
  mainWindow = new BrowserWindow({
    height: 560,
    useContentSize: true,
    width: 680,
    webPreferences: {
      nodeIntegration: true
    }
  })
  mainWindow.loadURL(winURL)
  mainWindow.on('closed', () => {
    mainWindow = null
  })
});
```

```App.vue
<script>
const electron = require('electron');
const { ipcRenderer } = require('electron');
const remote = electron.remote;

export default {
   mounted(){
      //引数でファイル指定があったらそれを開く
      if(remote.process.argv.length > 1) {
        var p = remote.process.argv[1];
        if(p.match(/\.[a-zA-Z]+$/)) this.load(p);
      }

      //Macでファイル指定があったらそれを開く
      if(remote.process.openFile !== void 0) {
        if(remote.process.openFile.match(/\.[a-zA-Z]+$/)) this.load(remote.process.openFile);
      }
      ipcRenderer.on('Load',(event,path) => this.load(path));
   }
}
</script>
```