ボタンを長押ししたら、ショートカットキーのヒントを表示する

⌘キーを長押しした時、ショートカットのヒントアイコンを表示する

SlackのMacアプリが
⌘キーを長押しすると「⌘キー+数字キー」のショートカットの
ヒントを表示するなんてことをやってまして、

→ (ふわっと出てくる)


これは良いUIだと思ったのです。


要件

  • ⌘キーを長押ししたらアイコンを表示する
  • キーを離したらアイコンを消す


実装

こんな感じかな?

  • CSS変数で表示を切替。
  • ボタンを押してから、1秒後にアイコンを表示する。
  • ボタンを放したら、アイコンを消す。もしくはアイコン表示のアニメーション(?)を止める。
  • フォーカスが外れた時もアイコンを消す処理を入れておいた方が良さそう
    • これで長押しを表現。
    • 今はタイマーで表示制御とか、要らないんですね。


<html>
<head>
    <style>
        button {
            width: 200px;
            height: 50px;
        }

        :root {
            --shortcut-hint-delay: 1s;
            --shortcut-hint-opacity: 0;
        }
        .shortcut-hint-container {
            position: relative;
        }
        .shortcut-hint {
            position: absolute;
            width: 10px;
            left: 3px;
            top: calc(100% - 12px);
            z-index: 1;
            vertical-align: middle;
            text-align: center;
            line-height: 10px;
            font-size: 9px;
            color: white;
            background-color: black;
            border-radius: 2px;
            transition-delay: var(--shortcut-hint-delay);
            opacity: var(--shortcut-hint-opacity);
        }
    </style>

    <script>
        var hintDisplayed = false;

        window.addEventListener('keydown', function(e) {
          if (e.metaKey) {
            hintDisplayed = true;
            document.documentElement.style.setProperty('--shortcut-hint-delay', '1s');
            document.documentElement.style.setProperty('--shortcut-hint-opacity', '1');
          }
        });
        window.addEventListener('keyup', function(e) {
          if (hintDisplayed) {
            hintDisplayed = false;
            document.documentElement.style.setProperty('--shortcut-hint-delay', '0s');
            document.documentElement.style.setProperty('--shortcut-hint-opacity', '0');
          }
        });
        window.addEventListener('blur', (e) => {
          if (hintDisplayed) {
            hintDisplayed = false;
            document.documentElement.style.setProperty('--shortcut-hint-delay', '0s');
            document.documentElement.style.setProperty('--shortcut-hint-opacity', '0');
          }
        });
    </script>
</head>
<body>
    <div class="shortcut-hint-container">
        <button>保存ボタン</button>
        <label class="shortcut-hint">S</label>
    </div>
</body>
</html>



おわり

おわりだん。

AutoMator Actionの「Finder項目にフィルタを適用」はキャッシュを持っている(みたい)

遭遇した問題

MacAutoMatorという作業自動化アプリがあるのだが、

そのAutoMatorで使用できる、
「Finder項目にフィルタを適用」というアクションは、


(おそらく)実行結果のキャッシュを持っていて、
何度も何度も繰り返し処理を実行するような場合、
最新のフィルタリング結果が次のアクションに渡されない。

過去の実行結果が渡ってしまう。


よって、例えば、フォルダーアクションなどで、このフィルタリングを使うと、
・フォルダーにファイルを追加しても検知されない、
・その追加したファイルに対する処理がスルーされる、
などの問題がしばしば発生する。
発生した。


(これがうまくいかない)


対応方法

この実行結果のキャッシュの問題は、
「Finder項目にフィルタを適用」を「シェルスクリプトを実行」に置き換えて、
シェルスクリプトでフィルタリングしてしまえば解決する。

# 拡張子wavのファイルに絞って、次のアクションに回すシェルスクリプト
while read line; do
    if expr "$line" : ".*.wav$" > /dev/null
    then
        echo "$line" >&1
    fi
done


(こんな感じに)


おわり

案外、この問題には苦戦した。

Electronアプリの起動時間短縮に挑む

アプリが起動するまでの時間が短いと気持ちいい

アプリの起動は最初の一回しか発生しないのに、
その後はアプリは起動しっぱなしなのに、
この最初の起動までの時間が短いと、アプリの使用感が非常に良くなるような気がする。 気がしない?


この記事はElectronアプリの起動速度を速くするために
いろいろやったことと、その結果の記録です。
環境はMacのElectronです。


なお計測はしていたが、記録はない。
各仕組みの導入の前後で結構コードが変わってしまって、
計測の仕方も変わってしまって、
あとバイナリは残っているから調査可能だけど、面倒くさいからであります。


初回起動時の記録は捨てとこう

ところでElectronのアプリって、
ビルド後初回の起動より、2回目・3回目の方が早く起動しますね。
計測して起動時パフォーマンスチューニングするなら、初回の記録は捨てときましょ。


やったことリスト

  • require文の書き換え・遅延ローディング
  • ファイルの統合・分割・minify
  • コールバック・Promise
  • 設定ファイルを同期・非同期で読み込む
  • 設定ファイルの読み込みのタイミングをずらす
  • Electronのバージョンを変更する
  • アプリ起動時の見た目をごまかす


require文の書き換え・遅延ローディング

require文を書き換えて、ライブラリを遅延ローディングするように変更する。
すごく効く。効いた。

var log = require('electron-log');
log.error('error found.');

↓例えば、こう書き換える

var _log, log = () => { _log = _log || require('electron-log'); return _log; };
log().error('error found.');


ファイルの分割・統合・minify

Electronアプリで使用しているファイルを、

  • Webpackなどで1つにまとめたり、
  • 逆に細かく分割してファイル1つあたりを軽量にしたり、
  • ファイルをminifyしてファイルのサイズを小さくしたり。

asarに固めたElectronアプリでは効かない

全然全然本当にまったく効果なかった。
まったく効果がないので、余計な手間がビルド時に発生する分、マイナスまである。

何度となく諦めきれずに試したが、そのたびにまったく効果がないと知ることとなった。
でも、私はきっとまた試すよ。


コールバック・Promise

コールバックとPromiseは、コールバックの方が早い。
世間の流れと逆方向でありますね。

でも、仕方ない。
アプリの起動時には、ほんの僅かでも速度が欲しいのであります。
効く

// Promise
hoge().then((data1) => {
  foo(data1).then(data2) => {
    // 何かやる
  });
});
// コールバック
hoge((data1) => {
  foo(data1, (data2) => {
    // 何かやる
  });
});


設定ファイルを同期・非同期で読み込む

設定ファイルを読み込む処理があるとして、

  • 非同期で設定ファイルを読み込むより、
  • 同期で設定ファイルを読み込む方がかなり早い


早いが、もちろんデメリットあり。
アプリの処理によっては、処理がブロックされて逆に遅くなるでしょう。

更に更に、
例えば、設定ファイルがJSONであるなら、
こんな感じにrequire文を使って読み込むと、
同期での設定ファイル読み込みより更に早かったのですけど、
これはセキュリティ的な問題があるでしょうな。

var dataJson = null;
try {
  // data.jsonの読み込み
  dataJson = require(`${app.getPath('userData')}/data.json`);
} catch (e) {
  dataJson = [];
}
// 読み込み終わったらキャッシュ消しておく
delete require.cache[`${app.getPath('userData')}/data.json`];


設定ファイルの読み込みのタイミングをずらす

私はAngularを使用しているのですけど、
Renderer Processで画面を描画中に設定ファイルを読み込もうとすると、
同期であろうと非同期であろうと、ものすごく遅くなる。

その描画中の範囲を外して設定ファイルを読み込むようにすると
めちゃくちゃ早くなる。
とても良く効く


自分の環境では、同じ処理が実行タイミングの違いで、これくらい効いた。

1秒 → 0.002秒


Electronのバージョンを変更する

少し効く
Electronのバージョンを3から4にする。
何度も計測した結果、ちょっぴり早くなっていると思う。
ちょっぴり。

でも、Electronのバージョンは、あまり選択可能な要素ではないですね。

私はまだElectron4で安定動作させられなかったので、
Electron3に戻しちゃいましたけど、
いろいろ問題解決したら、もちろんElectronのバージョンを上げておきたい。

もうElectronの5が出てる?気のせいでしょ。


アプリ起動時の見た目をごまかす

設定ファイルからデータを読み込んで、 読み込み終わった順にそのデータでビューを更新すると、
画面がちらちら更新されて、体感、アプリの起動が遅く感じる。

一括でパッと表示した方が早く感じる。操作できるまでの時間変わらなくても。

設定ファイルの読み込み順と、読み込みタイミングの考慮は要るけれど、
手間暇かけると、アプリの起動時間の体感は良くなる。


おわり

おわりだよー。

gulp3のrun-sequence + エラー処理なコードを、gulp4で置き換える

いろいろ

gulp3にタスクを連続で実行するrun-sequenceというライブラリがありました。

gulpのバージョン4以降では、このrun-sequenceの代わりに、gulp.seriesを使うことになったのだが、 gulp.seriesには、run-sequenceにあった、発生したエラーを受け取るcallbackが用意されていませんでした。

try-catch文を使っても、gulp.series内で発生したエラーを拾うことはできない。
では、gulp3 → gulp4と書き換える際、run-sequenceをどのように書き換えるべきか。


gulp3 + run-sequenceのコード

run-sequenceでは、こんな感じで発生したエラーを拾えていた。

const gulp = require('gulp');
const runSequence = require('run-sequence');

gulp.task('package', (cb) => {
  runSequence('tsc-debug', '_rm-package', '_package-debug', (err) => {
    if (err) {
      gulp.start('_notifyError');
    }
    cb(err);
  });
});


gulp4 + gulp.seriesのコード

エラーを拾うコールバックがgulp.seriesに用意されていないので、
すんなりgulp4に書き換えられない。 エラー処理が抜けてしまう。

const gulp = require('gulp');

gulp.task('package',
  gulp.series('tsc-debug', '_rm-package', '_package-debug'));


gulp.seriesのコードを追ってみる

gulpの各タスクの処理はtaskWrapperで包まれる。
// node_modules/undertaker/lib/set-task.js
function taskWrapper() {
  return fn.apply(this, arguments);
}
gulp.seriesを作成時、createExtensionsが呼ばれる。
// node_modules/undertaker/lib/series.js
function series() {
  var create = this._settle ? bach.settleSeries : bach.series;
  var args = normalizeArgs(this._registry, arguments);
  var extensions = createExtensions(this); // ←←←←←←←←←←←←←←←←←←←
  var fn = create(args, extensions);
createExtensionsで、エラー処理のハンドリングが実装されている
  • エラー発生時にerrorイベントが発行される
// node_modules/undertaker/lib/helpers/createExtensions.js
function createExtensions(ee) {
  return {
    error: function(error, storage) {
      if (Array.isArray(error)) {
        error = error[0];
      }
      storage.release();
      ee.emit('error', { // ←←←←←←←←←←←←←←←←←←←
        uid: storage.uid,
        name: storage.name,
        branch: storage.branch,
        error: error,
        duration: process.hrtime(storage.startHr),
        time: Date.now(),
      });
    },
最終的にgulpでエラーを拾える
  • つまり、gulp.series内で発生したエラーは、次のようなコードで拾える。
    • gulp.on('error', エラー処理)
const gulp = require('gulp');

gulp.on('error', (err) => { // ←←←←←←←←←←←←←←←←←←←
  console.log('--- catch error');
});


gulp.seriesで発生したエラーを拾うコードの例(全体)

const gulp = require('gulp');

gulp.task('hello', (cb) => {
  console.log('hello');
  return cb();
});

gulp.task('world', (cb) => {
  console.log('world');
  return cb();
});

gulp.task('gulp', (cb) => {
  console.log('gulp');
  throw new Error('error from task "gulp"');
});

gulp.on('error', (err) => {
  console.log('--- catch error');
});

gulp.task('run', gulp.series('hello', 'world', 'gulp'));


npmのモジュールにしてみた

  • gulp3 run-sequenceの処理を、なるべくそのままgulp4.seriesに置き換えれるようにするnpmモジュール
npm install --save-dev gulp-task-error-handler


gulp3 run-sequenceのコードを、gulp4 gulp.seriesで置き換える例
  • gulp3 run-sequence code
    • 例えば、このようなコードがあったとして、
const gulp = require('gulp');
const runSequence = require('run-sequence');

gulp.task('package', (cb) => {
  runSequence('tsc-debug', '_rm-package', '_package-debug', (err) => {
    if (err) {
      gulp.start('_notifyError');
    }
    cb(err);
  });
});
  • replace with gulp-task-error-handler sample.
    • このモジュールを使うと、このように置き換えられる。
const gulp = require('gulp');
const handleError = require('gulp-task-error-handler');

gulp.task('package',
  handleError(gulp.series('tsc-debug', '_rm-package', '_package-debug'), (err) => {
    gulp.task('_notifyError')();
  })
);


おわり

おわりなのだ。

MacのElectronで"safeDialogs"の機能が動作するようになった

要約

動作しないと自分の中で評判だった、
Electronの無限アラート防止機能"safeDialogs"の問題が修正。
Electronバージョン5から動作するようになった。


BrowserWindowのsafeDialogsオプション

ElectronのBrowserWindowに、"safeDialogs"というオプションがあります。
このオプションは、例えば、
アラートダイアログが無限に繰り返し表示されるような処理が見つかった時、
そのループを停止してくれる機能です。

safeDialogs Boolean (optional)
- Whether to enable browser style consecutive dialog protection. Default is false.

https://electronjs.org/docs/api/browser-window


動作サンプル


こんな感じの無限アラートを、
Electronのrendererプロセスに仕込んで、

while (true) alert('!')


BrowserWindowのパラメータで、"safeDialogs"を指定しておくと、

function createWindow () {
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      safeDialogs: true, // ←←←←←←←←←←←←
      safeDialogsMessage: 'stop dialog',
      nodeIntegration: true
    }
  })


無限アラートを検知した時に、
ループを停止するチェックボックスが表示されます。

ただし、Macでは長らく動作していなかった。

この機能、オプション導入直後から、
MacのElectronでは長らく動作していませんでした。
誰も試してなかったのかな。

無限アラートがあっても、スルーされてました。
https://github.com/electron/electron/issues/17543


これで安心

たとえ、兵庫県に住んでいてもね!!

続・Final Cut Pro XでWAVファイルにロールを自動割り当てする機能の仕様を特定した(してない)

要約

ぜんぜん特定してなかった(´д`)


前回
Final Cut Pro XでWAVファイルにロールを自動割り当てする機能の仕様を特定した https://taku-o.hatenablog.jp/entry/2019/05/25/060055


判明した問題

  • mixdownなWAVファイル、
  • ステレオなWAVファイル、

つまり、複数のチャネルを持ったWAVファイルに、
前回、「これで良いんだよ」と紹介した構造の、
次のようなiXMLチャンクを追加すると、

<BWFXML>
    <IXML_VERSION>1.27</IXML_VERSION>
    <TRACK_LIST>
        <TRACK_COUNT>1</TRACK_COUNT>
        <TRACK>
            <CHANNEL_INDEX>1</CHANNEL_INDEX>
            <INTERLEAVE_INDEX>1</INTERLEAVE_INDEX>
            <NAME>dummy-track</NAME>
        </TRACK>
    </TRACK_LIST>
</BWFXML>


Final Cut Pro XにWAVファイルを取り込んだ時に、こうなってしまう。
空のオーディオロールが振られてしまうことが判明したのです。

<空白>
dummy-track


でもでも?

しかしながら、ではこうしたらと、
次のように、WAVファイルのiXMLチャンクに、
チャネル数のトラック情報を入れたとしても、

<BWFXML>
    <IXML_VERSION>1.27</IXML_VERSION>
    <TRACK_LIST>
        <TRACK_COUNT>2</TRACK_COUNT>
        <TRACK>
            <CHANNEL_INDEX>1</CHANNEL_INDEX>
            <INTERLEAVE_INDEX>1</INTERLEAVE_INDEX>
            <NAME>dummy-track</NAME>
        </TRACK>
        <TRACK>
            <CHANNEL_INDEX>2</CHANNEL_INDEX>
            <INTERLEAVE_INDEX>2</INTERLEAVE_INDEX>
            <NAME>dummy-track</NAME>
        </TRACK>
    </TRACK_LIST>
</BWFXML>


今度は、このように無駄なナンバリングサフィックスのついた、
オーディオロールが振られてしまうようなのです。

dummy-track-1
dummy-track-2


この場合、できれば、
Final Cut Pro X上で、手動でオーディオロールを設定した時と同じように、
dummy-trackという名前でロールを1つ設定して欲しいのですね。
そう、手動でなら設定できる。


iXMLチャンクでWAVファイルのオーディオロールの情報を設定しようとした時、
うまく設定できるiXMLのデータ構造がどうにも分からない。


結論

どうすれば良いか、わからぬ・・・
わからぬ・・・

Final Cut Pro XでWAVファイルにロールを自動割り当てする機能の仕様を特定した

概要

Final Cut Pro Xには、オーディオファイルを読み込んだ時に、オーディオロールを自動的に設定する機能があります。

なのですが、その仕様の詳細がよくわからなかった。

このたび、試行錯誤の末、なんとか動作する仕様を特定したので、
まとめておきます。

Final Cut Pro X: オーディオレーンを使ってタイムラインを整理する https://support.apple.com/kb/PH12652

Final Cut Pro X: 読み込み時に割り当てられるオーディオロールを設定する https://support.apple.com/kb/PH26179


特定した仕様

iXML Chunkの構造

まず、iXML Chunkの定義。
次のようなChunkの構造を、wavファイルの最後に突っ込む。
wavファイルに含まれるRIFF Chunkにはファイルサイズの項目があるから、既存のwavファイルにiXML Chunkを追加する時は、そちらのサイズも増やさないとダメです。

bytes 定義 入れるデータ
1-4 Chunk ID iXML
5-8 Chunk Size iXML Chunkの長さ
9- iXML Meta Data iXMLメタデータ


iXMLメタデータ

Final Cut Pro Xで動作する、最小のiXMLメタデータの定義は次の通りです。
BWFXML/TRACK_LIST/TRACK/NAMEの位置(audio role nameと入っている場所)で、自動割り当てしたいオーディオロール名を指定します。
("&"、"<"、">"を値に使う場合はエスケープが必要。)

<?xml version="1.0" encoding="UTF-8"?>
<BWFXML>
    <IXML_VERSION>1.27</IXML_VERSION>
    <TRACK_LIST>
        <TRACK_COUNT>1</TRACK_COUNT>
        <TRACK>
            <CHANNEL_INDEX>1</CHANNEL_INDEX>
            <INTERLEAVE_INDEX>1</INTERLEAVE_INDEX>
            <NAME>audio role name</NAME>
        </TRACK>
    </TRACK_LIST>
</BWFXML>


拡張子

確認した限りでは、ファイルの拡張子は.wavでないと動作しない。
正直、別の拡張子を当てたかった(-_-;)
.bwfとか。


割り当てられるオーディオロール

ドラッグアンドドロップで、wavファイルをインポートする場合、
audio subroleはiXMLチャンクで指定できますが、親のオーディオロールは
指定できない(断言)
デフォルトでは、ダイアログ(dialogue)の子ロールに割り当てられてしまう。


これを変えたい場合は、

  • メディアの読み込み機能からインポート時にロールを指定するか、
  • 環境設定でデフォルトで割り当てるロールを変更する、と良い。


ロールの編集で、ロールをドラッグすると、ロールどうしをマージできるので、
一度、取り込んでから、正しいロールに統合するのでも良いとは思います。
でも、一時でも、ダイアログ(dialogue)に割り当てられちゃうと面倒くさい使いづらい


その他、やったこと

まず、npmモジュールを作ってみた

npmモジュールを作った。
このFinal Cut Pro X用の、iXMLチャンクをwavファイルに追加できる。 https://www.npmjs.com/package/fcpx-audio-role-encoder


フォルダアクションでwavファイルを変換

作ったnpmモジュールを利用して、
wavファイルを変換するAutoMatorのアクションと、
フォルダに入れられたwavファイルを変換処理するフォルダアクションを作った。
https://github.com/taku-o/fcpx-audio-role-workflow


これで、フォルダに音声ファイルを突っ込むだけで、
ファイル名のパターンに従って、
オーディオロールが自動設定されるようになったのである。
めでたしめでたし。


なんだか、めずらしく役に立つ話、した気がする!!!