思索的逍遥の記。

いろいろな考え事。

YukkuLips の使い方 - クイックスタート

まえがき

少し前から、YukkuLipsという拙作アプリケーションを公開していた。

github.com

このアプリケーションがどんなものなのかはREADMEに書いてあるが、簡単に言えば、macOS上でキャラ素材をアニメーションさせる動画の作成を、何とか実現するためのツールである。機能は、キャラ素材が音声に合わせてアニメーションしているBB素材を作ることのみに特化している(動画ではなく画像の出力も可能)。

本当は、このツールの使い方を紹介する動画を早い段階で作成し、投稿しようと考えていた。しかし現状、時間と体力の不足により、動画作成を延々と先延ばしにしてしまっている。

一方、ときどきYukkuLipsを使おうとしてくださる方々がいらっしゃり、十分な説明を提供できていない事に歯痒さを感じている。そこで、とにかく説明を早期に公開するために、YukkuLipsの使い方について、動画ではなく、まずは記事を書く事にした。

クイックスタート

今回は細かいことは置いといて、まずは使い始めてみよう、という内容である(後日、より詳細な記事を少しずつ公開する予定)。

ダウンロード

以下のリンクから、最新版(最も上に表示されている)YukkuLips-darwin-x64.zipをクリックしてダウンロードする(記事執筆時点の最新版は"YukkuLips v0.2.4"である)。

Releases · PickledChair/YukkuLips · GitHub

インストール

READMEに記載の通り。添付のREADMEには重要なことも書いてあるので、必ず読んでいただきたい

(既にインストール済みの状態から最新版にアップデートしたい場合、単純に、インストール場所の古いアプリを最新版に置き換える。)

初回起動

READMEのインストールの項目に記載の通りだが、YukkuLipsは開発において署名も公証も行っていないので、macOSのセキュリティ設定によって単クリックでは開かない様になっている(ゴミ箱に移す様に誘導される)。起動するには、Finder上で直接アプリアイコンを右クリックして、「開く」をクリックする。そして一度では起動しないはずであり、もう一度同様にすれば開く。

(署名や公証を通すのは現在の開発方法では難しいため、他の開発方法を検討中。)

プロジェクトの作成

アプリケーションの起動時にまず、「YukkuLipsへようこそ!」というダイアログが表示される。右側の「新規作成」ボタンを押すと、「新規プロジェクト」というタイトルのダイアログが開かれる。上の欄にプロジェクト名、下の欄に保存場所を入力する。保存場所の入力の時、右側に表示されている「...」ボタンを押すと、保存場所を選択するダイアログが現れるので、それを使うのが手っ取り早い。

f:id:sutukeisu:20201017205950p:plain
「新規プロジェクト」ダイアログ

OKボタンを押すと、プロジェクトが作成される。

なお、これは実際には、保存場所にプロジェクト名のフォルダを作成する作業である。あとでプロジェクト保存時に、その直下に project.json というファイルなどが生成される。このフォルダ内に、今後の編集情報が保存されていく。

(今回は動画生成を試してみるだけなので深く考える必要はないが、実際に投稿するための動画を作成する場合には、まずその動画のためのフォルダを作り、その下にYukkuLipsのプロジェクトフォルダを作った方が良い。今後の記事で詳しく触れるが、単にYukkuLipsのプロジェクトフォルダだけを作ると混乱の元となる可能性がある。)

キャラ素材の読み込み

キャラ素材を読み込むためにはキャラ素材が必要である。なので、まずはキャラ素材のダウンロード。キャラ素材スクリプト開発者のズーズ氏が管理されているnicotalk&キャラ素材配布所からダウンロードする(他にも配布場所はある。お好きなものをどうぞ)。このとき、YukkuLipsがキャラ素材スクリプトVer4系対応のキャラ素材までにしか対応していないことに注意する。例えば、新きつねゆっくりシリーズはVer5系対応のキャラ素材なので、新れいむ等のキャラ素材は正常に読み込めない。あらかじめ、キャラ素材が対応しているキャラ素材スクリプトのバージョンを確認すること。また、キャラ素材スクリプトの規約と、キャラ素材自体の規約の確認も行うこと(重要)

追記:Ver1系のキャラ素材はアニメーション画像のファイル名の付け方が異なる。本家のキャラ素材スクリプトではそれをVer2以降の仕様に自動修正して読み込んでいる。YukkuLipsでは自動修正がないので、きつね氏による説明記事等にしたがってファイル名を振り直して使うと良い。)

(以下の画像は「きつねゆっくり」「新きつねゆっくり」それぞれの配布ページから引用。)

f:id:sutukeisu:20201017203837j:plain
⭕️ きつね氏の「きつねゆっくり」シリーズは「Ver4a」対応であり、YukkuLipsで扱える

f:id:sutukeisu:20201017204743j:plain
❌ きつね氏の「新きつねゆっくり」シリーズは「Ver5f」対応なので、YukkuLipsはサポートできていない

(今日のキャラ素材動画文化は先述のズーズ氏と、キャラ素材作者のきつね氏によって開始された。ご両人に対し十分なリスペクトを持ってキャラ素材動画を作成すること。)

ダウンロードしたキャラ素材を適当な場所に解凍する。それからYukkuLips側で、ウィンドウ右上のキャラ素材リストというラベルの右側にある「+」ボタンを押す。フォルダの選択ダイアログが現れるので、キャラ素材名が書かれているフォルダを選択して「Open」ボタンを押す。すると、YukkuLipsのメインウィンドウには次の変化がある。

  • キャラ素材リストには、キャラ素材名が書かれたサムネイル付きの項目が追加される
  • レイアウトプレビューには、ブルーの背景の画面(つまりブルーバック、BB)の左上にキャラ素材が表示される
  • 下半分のシーンブロックリストには、キャラ素材の名前が書いたカラムと、行番号0の空白の項目が追加される(この項目を「シーンブロック」と呼ぶが、詳しい解説は後の記事に譲る)

f:id:sutukeisu:20201017210437p:plain
きつね氏のキャラ素材「れいむ」を読み込んだ時のウィンドウ

キャラ素材の位置合わせ

動画の画面内で、キャラ素材がどこに表示されるのかを示しているのが、レイアウトプレビューである。左上を原点としているので、読み込んだ直後のキャラ素材の初期位置は左上である。

このままだと動画として奇妙なので、キャラ素材の位置を決めよう。レイアウトプレビューの右側にある「レイアウト編集」ボタンを押すと、「レイアウト編集」ダイアログが開く。左側に表示されているキャラ素材は直接ドラッグして位置を変更できる

もし今の段階で動画の背景として使う画像が決まっていれば、その画像を読み込んで、キャラ素材の位置決めの参考に使うことができる(ただし、あらかじめ16:9のアスペクト比に編集している必要があるので要注意)。セリフの表示箇所をすでに決めていれば、なお便利である。参考のための背景画像は、右下の方にある「背景画像を設定」ボタンで設定できる。

(筆者はKeynoteを使って画像を作っている。スライドのデフォルトのアスペクト比が16:9になっており、それを画像として書き出せば必要な画像が得られる。)

f:id:sutukeisu:20201017211334p:plain
「レイアウト編集」ダイアログ。背景素材(ニコニコモンズ・nc13676)とセリフ枠素材(ニコニコモンズ・nc50241)をKeynoteで組み合わせて、スライドを画像出力し、それを背景として適用している

調節し終わったら「適用して閉じる」ボタンを押す。するとレイアウトプレビュー中のキャラ素材が、自分が設定した位置に移動する。

ここで、背景が青色のままなのは奇妙に思えるが、これは仕様である。最初に触れたが、YukkuLipsはあくまでBB素材を作るためのアプリケーションであり、背景付きの動画を出力する機能はない。したがって、後で出力する動画にも背景は統合されない。背景はYukkuLipsでの編集作業後、別の動画編集ソフトを用いて設定する事になる

セリフの設定

セリフを設定するにはシーンブロックを編集する。シーンブロックリストには現在、一つのシーンブロックがあり、そのシーンブロックには選択中であることを示す水色の背景色が付いている。この状態で「シーンブロック編集」ボタンを押すと、「シーンブロック編集」ダイアログが開く。

f:id:sutukeisu:20201017213227p:plain
「シーンブロック編集」ダイアログ。後の説明にしたがって、セリフ欄にセリフを書き込んでいる。

現在このプロジェクトで読み込んでいるキャラ素材は「れいむ」だけなので、ダイアログ上方のリストには「れいむ」だけが表示されており、この「れいむ」が選択状態となっている。このリストの下に表示されている情報は、現在選択状態になっているキャラ素材の情報である。

「セリフ」の欄が空欄になっているので、キャラ素材に喋ってもらいたいセリフを書き込もう。ただし、ここで書き込んだセリフは単にテキストとして保存されるだけで、このままでは字幕にもならないし、音声にも変換されない。YukkuLipsには字幕の生成機能もなければ、ゆっくりボイスの生成機能もないのである。字幕は他の動画編集ソフト等で編集し、音声はゆっくりボイスを生成する別のソフトで出力することを想定している。

では何のためにセリフを書き込むのかというと、台本の保存と、シーンブロックリストでの一覧表示のためである。ここで一度、「適用して閉じる」ボタンを押してみよう。シーンブロックリストに、今打ち込んだセリフが表示されているのがわかる。これにより、このシーンブロックが台本のどのセリフに対応しているのかがわかるようになっている。

(今後の記事で取り上げたいと思うが、打ち込んだセリフは結局のところ、今回紹介しないYukkuLipsの機能を使う上で重要になってくる。)

ゆっくりボイスの関連付け

先ほどの説明の通り、ゆっくりボイスを出力するには、そのための別のソフトが必要である。現状、macOS上で使いやすいゆっくりボイス生成ツールとしては、次の二つが挙げられる(素晴らしい!)。

他の手段がある場合はそれを用いても良い(ただし、用意する音声のファイル形式はWAVEかMP3である必要がある)。今回はソフトのダウンロードが不要な「ゆくも!」を用いる。ちなみに、YukkuLipsはMYukkuriVoiceとの連携機能もあり、それについては今後の記事で解説予定である。

「ゆくも!」のページを開いたら、テキストフィールドにセリフを打ち込む。再生ボタンで試聴できるので、目的の発音になるまで入力を修正する(上の方にあるメニューでHomeではなくProを選択すると、より細かい設定が可能である。発音記号の仕様はゆっくりボイスもといAquesTalkの開発元・アクエスト社が公開しているPDFで確認できる)。できたら、ダウンロードボタンを押してダウンロードする。

f:id:sutukeisu:20201017224824p:plain
デフォルトで生成される音声の発音が変だったので、筆者はメニューから"Pro"を選択して発音記号を編集した。

ダウンロードした音声の保存場所はどこでも問題ないが、とりあえずプロジェクトフォルダの下に「音声」というフォルダを作り、そこに入れるようにしておこう。

f:id:sutukeisu:20201017214257p:plain
ここまで手順通りに行った場合、作成したYukkuLipsプロジェクトフォルダをのぞくと、"scene_block"という空のフォルダしかまだ存在していない。この隣に「音声」というフォルダを作った。フォルダはここに作らなければいけないわけではなく、音声はどこに保存しても良い。

YukkuLips側では、もう一度「シーンブロック編集」ボタンを押して「シーンブロック編集」ダイアログを表示する。先ほどのセリフ欄の下に、セリフに対応する音源のパスを入力する項目がある。「アニメ生成用音源」の項目の「ファイルの選択」ボタンを押すと、ファイル選択ダイアログが現れるので、これで保存しておいた音声ファイルを選択する。すると、その下の「動画用音源」の項目も自動で同じパスが入力される。この二つの項目の違いについては、今後の記事で触れたいと思う。正しい音源ファイルが指定できているかどうかは、「再生」ボタンを押してファイルを再生することで確かめられる。

f:id:sutukeisu:20201017214958p:plain
「ファイルの選択」ボタンを押して、先ほど「ゆくも!」で作成した音声ファイルを指定する。「アニメ生成用音源」だけでなく「動画用音源」の欄にもファイルのパスが反映されるはずである。この両方ともに、ファイルパスが記載されていなければならない。

動画素材の保存

ここまで来れば、すぐに動画を生成することができる。「シーンブロック編集」ダイアログの右下にある「動画を出力」ボタンを押すと、何かいろいろやってそうな進捗表示がしばらく出てくるので、それが終わるまで待つ。終わったら、生成した動画がQuickTime Playerで自動で開かれるので、それを見て出来を確認しよう。

それから、すぐにこの動画のウィンドウを閉じないで、ファイルメニューから「複製」を選べば、この動画の複製を作ることができる。作った複製は、タイトルバーにおいて保存場所やファイル名を変更できるので、好きなところに移そう。

f:id:sutukeisu:20201017220010p:plain
QuickTime Playerの「ファイル」メニューから、「複製」を選んで動画ファイルのコピーを作る。コピーした動画のタイトルバーで保存場所を編集できるので、好きな場所に動画を移そう。

(シーンブロックが二つ以上ある場合は、シーンブロックリストの「動画を結合して保存」ボタンを使うことができるが、今回は一つしかシーンブロックがないので、QuickTime Playerのファイルメニューから複製する様にしよう。)

プロジェクトの保存

YukkuLipsのメインウィンドウのタイトルバーを見るとわかるように、YukkuLipsプロジェクトは手動で保存操作をしない限りは未保存のままである。ファイルメニューで「保存」を押すか、ショートカットキー「⌘S」を押すことで保存できる。

f:id:sutukeisu:20201017220411p:plain
未保存のときのタイトルバー

f:id:sutukeisu:20201017220500p:plain
保存した後のタイトルバー

動画編集ソフトでの使用

YukkuLipsで行う作業はここまでであり、あとは出力した動画素材を用いて、他の動画編集ソフトで動画編集を行う。筆者は金欠なので無料のiMovieを使う(そのうちFinal Cut Pro等を購入したい)。

メジャーな動画編集ソフトは大抵クロマキー合成機能を備えている。幸運にもiMovieも同機能を備えているので、それを使おう。

その前にKeynoteで、背景を作る時に作ったスライドをもう一度開く。そのスライドを複製して、複製したスライドにセリフを書き込む。これを字幕として使う。なぜiMovieで字幕を編集しないのかというと、iMovieの字幕機能が貧弱だからである。もっと字幕機能がしっかりしている他の動画編集ソフトをお使いの場合は、そちらで字幕を編集していただいても差し支えない。

f:id:sutukeisu:20201017221903p:plain
このようにKeynoteを使うと、セリフの流れを追うのにスライド再生が使えて便利だという側面もある。

Keynoteで作ったスライドを画像として書き出すには、メニューから「ファイル → 書き出す → イメージ...」と選択する。

素材が揃ったら、iMovieの出番である。iMovieを立ち上げ、新しいプロジェクトを作成したら、メディアブラウザの欄にキャラ素材入りのBB素材と、別に用意した背景素材(ここではセリフ付き背景)を突っ込んでおく。そしてまずは背景をウィンドウ下部のタイムラインにドラッグ&ドロップする。

f:id:sutukeisu:20201017223621p:plain

この背景は初期設定ではズームインしてしまうので、その設定を切っておく。

f:id:sutukeisu:20201017223721p:plain
矢印で指している「すべてをリセット」を押すと、余計なズームインが解除される

その上にキャラ素材入りの動画素材を載せると、青背景でキャラ素材が喋っている動画になる。あとはこの青背景を透過させれば良い。

f:id:sutukeisu:20201017223847p:plain
先ほどタイムラインに配置した背景の上に、BB素材を載せることができる。先頭を揃えておくと良い。音声のタイミングを揃えたいときなどは、音声の波形が表示されているのでそれを目安にすると良い。

右上の画面の上にあるアイコンのうち一番左のアイコンで、「グリーン/ブルースクリーン」を選択する。すると青背景が透過し、後ろの背景が見えるようになる。

f:id:sutukeisu:20201017224048p:plain
これでクロマキー合成ができる。

あとはプレビューしつつ細かいところを調節すれば良い。もし字幕と音声のタイミングが合っていない場合は、合わせた方が自然だ。下のタイムライン表示では、音声の鳴り始めを波形表示で判断できるので、それを目安に字幕のタイミングを合わせると良い。

f:id:sutukeisu:20201017224120p:plain
動画の書き出しは、右上の共有ボタンから「ファイルを書き出す」を選ぶことでできる。

あとがき

以上、駆け足でYukkuLipsの使い方を解説した。ここまで読んでお気づきかと思うが、この手順を愚直に繰り返すとかなり面倒である。そこで、もう少しだけ作業を楽にする手段を用意してある。それについては今後の記事で触れていきたい。

また、まだ表情やアニメーションの設定方法について触れていない。これについても、今後の記事で書いていく事にする。

SpacemacsでC++17の入力補完をする(macOSの場合)

背景

最近、C++への再入門を試みている。何年も前に一度挑戦して挫折したのだが、RustやC言語を多少勉強した上で、今なら以前よりもC++のことを理解できるのではないかと思ったからだ。そして、せっかく再入門するのだから、新しめの仕様で勉強しようと思い、C++17を勉強することにした。

環境はmacOS Catalina、homebrewでgcc(バージョン9.3.0)をインストールしている。エディタはここ2年くらいSpacemacsを使っている。C/C++ layerがあるので、これを使おうと思った。

.spacemacsの設定

SPC f e d.spacemacsを開き、dotspacemacs-configuration-layersc-c++を付け足すのだが、その際多少設定を足す。

  • c-c++-default-mode-for-headersを'c++-modeに設定する:デフォルトでは.hファイルがCのヘッダファイルとして扱われるので、C++モードに変更する
  • c-c++-enable-clang-supportをtに設定するauto-completion layerを介して自動補完するために必要な設定

該当部分だけ抜き出すと下記のようになる。

dotspacemacs-configuration-layers
'(
  ...
  auto-completion
  syntax-checking
  (c-c++ :variables
         c-c++-default-mode-for-headers 'c++-mode
         c-c++-enable-clang-support t)
  ...
  )

llvmのインストール

上記までの設定ではコード補完が動作しない。ドキュメントを読むと、このlayerはデフォルトではバックエンドにlsp-clangdというものを使っている。lsp (Language Server Protocol) はコード補完のためにエディタをクライアント、言語環境をサーバーとして専用のプロトコルでやりとりさせるというものらしく、ここではC++コード補完用の言語サーバーとしてclangdが使われている。もちろんそんなものをインストールした記憶はないから、用意する必要がある。

clangd公式サイトで説明されているインストール方法を確認すると、macOSではbrew install llvmとだけすれば良いことがわかった。かなり楽だ。ただ、インストール直後に以下のようなメッセージが表示されるので、指示にしたがってパスを通す必要がある。自分は一番上のLDFLAGSの設定は無視して、他の3つを~/.zshrcに加えてパスを通した。

To use the bundled libc++ please add the following LDFLAGS:
  LDFLAGS="-L/usr/local/opt/llvm/lib -Wl,-rpath,/usr/local/opt/llvm/lib"

llvm is keg-only, which means it was not symlinked into /usr/local,
because macOS already provides this software and installing another version in
parallel can cause all kinds of trouble.

If you need to have llvm first in your PATH run:
  echo 'export PATH="/usr/local/opt/llvm/bin:$PATH"' >> ~/.zshrc

For compilers to find llvm you may need to set:
  export LDFLAGS="-L/usr/local/opt/llvm/lib"
  export CPPFLAGS="-I/usr/local/opt/llvm/include"

これでコード補完が動作するようになった。

.clang_completeファイルを書く

初めのうちは上記の設定で問題ないように感じていたが、肝心のC++17の機能に関して入力補完も効かないし、標準ライブラリに存在しないと怒られたりもするので、どうやら-std=c++17コンパイルオプションがコードチェックのツールに渡っていないようだった。

clangdGetting startedによると、compile_flags.txtというファイルにコンパイルオプションを一行につき一つ書いてプロジェクトのディレクトリにおいておくことで、それらのコンパイルオプションをコードチェック時に適用する。しかし今回はうまくいかなかった。

半ば諦めかけていたが、C/C++ layerのREADMEの下の方に、.clang_completeファイルを置くといい、と書いてあるのをやっと発見した。使い方はcompile_flags.txtと全く同じようだったので、とりあえず以下のように書いた。

-std=c++17
-Wall
--pedantic-errors

このように書いた.clang_completeをプロジェクトディレクトリ直下に置くと、ちゃんとC++17向けのコードチェック・コード補完が走るようになった。しばらくはこれで行きたいと思う。

参考文献

Pythonista をショートカットからURLスキームで起動する

以前、iOSのショートカットアプリからPythonistaを起動する方法について記事を書いたが、その記事で追記したように、iOS 13では引数の受け渡しが動作しない。

sutukeisu.hatenablog.com

その後Pythonistaはバージョン3.3にアップデートされたが、このバグは現在(2020/04/18)も残っている。将来的には修正されると思われるが、一方で、現時点でもショートカットアプリからPythonistaに引数を受け渡しつつ、Pythonスクリプトを実行することは可能である。どうするかというと、URLスキームを利用する。Pythonista 3.3は以前のバージョンよりURLスキームを利用しやすくなったので、せっかくだから上記記事で扱ったスクリプトを実行できるかどうか検証した。ちゃんと動作することを確認したので、その手順を以下に示していく。

共有シートから呼び出せるショートカットとして設定する

とりあえずショートカットアプリで、新しいショートカット を作成する。今回は「Google翻訳」という名前のショートカットにした。 ブラウザで外国語のページを開いている時、共有シート(たいてい上矢印のボタンで出てくる)からショートカットを呼び出せるようにしたいので、右上の「」ボタンで詳細設定画面を開いたあと、以下のように「共有シートに表示」を有効にし、共有シートタイプを「URL」に設定する。

(この設定は前回も行なっていたが、説明を省いていた。)

URLスキームを変数に代入する

特定のPythonistaスクリプトを起動できるURLを手に入れた(後述)として、以下のように「テキスト」アクションにそのURLを貼り付け、それを「変数」アクションで、「URLスキーム」という名前の変数に紐付ける。

また、スクリプトに引数(例:hoge)を渡したいときは、&args=hogeという文字列を付け足すことで実現できる(参考:The Pythonista URL Scheme)。なので、今回はあらかじめ&args=という部分だけ別個に用意し、「変数」という変数に紐付けた。

(引数を2つ以上渡したい場合、コマンドライン引数の要領で&args=hoge fugaと半角スペースを挟むと、別個の引数として認識される……と言いたいところだが、上記参考ページにもあるように、手動エスケープ処理といった感じで、半角スペースを%20とする必要がある。よって&args=hoge%20fugaとすると良いだろう。)

さて、最初に「特定のPythonistaスクリプトを起動できるURLを手に入れた」という仮定を置いたが、どこからこれを仕入れれば良いのだろう? 参考ページにあった仕様に従って、自分でURLを書き出しても良いが、もっと簡単にURLを取得する方法がある。

まず、Pythonistaアプリで、ショートカットで起動したいスクリプトを開く。右上にレンチボタン(下画像の赤丸の中)があるので、これをタップする。するとメニューが開き、"Shortcuts..." というボタンがあるのでそれをタップする。

次に、表示されたリストの中から"Pythonista URL"を選び、タップする。

最後に、遷移したページの一番上にある"Copy URL"をタップする。

これでクリップボードに、現在開いているスクリプトを起動するためのURLがコピーされる。あとは前述したように、ショートカットの作成画面でそのURLを扱えば良い。

URLを引数に取らなかったときの処理を設定する

現状、ウィジェットボタンやショートカットアプリからの直接起動にも対応しているので、引数としてURLを与えられないケースがある。そのときのための処理を設定する。

if ... else ...のような構文にあたる「もし○○ならば〜その他の場合」アクションを使う。「〇〇」の部分は、「ショートカットの入力」の「値がない」というケースを指定する。if節には、「URL」アクションを置いて、変数「URLスキーム」を紐づける。「URLを開く」アクションにこの「URL」アクションを紐づければ、ショートカット動作時に、Pythonスクリプトが(引数を伴わずに)起動する。

(なお、「URL」アクションを使わずに、直接「URLスキーム」変数を「URLを開く」に入れることも可能だと思うが、あとで見た時のわかりやすさのためにこのような手順にしている。)

URLを引数に取るときの処理を設定する

あとはelse節で、本来の目的である「引数を取る場合」の処理を設定する。if節の「URL」アクションは変数「URLスキーム」のみを格納したが、これに変数「変数」と、「ショートカットの入力」(中身はウェブページへのURL)を結合してURLとしたい。そのためには、単純にこれらを「URL」アクションに横並びに入力すればよい。

以上で「Google翻訳」ショートカットは完成である。あとはスクリプト側で、sys.argvを使ってコマンドライン引数にアクセスできる。

自分のiPhone(XR)でこのショートカットが動作することを確認している。実際にはショートカットを経由しないGoogle翻訳スクリプトを自作して使用しているが……(下記の記事)。

sutukeisu.hatenablog.com

とはいえ、ショートカットアプリならSiriとの連携などが可能になるので、自作スクリプトの利用の幅はさらに増えるだろう。

Pythonista のエディタに置換機能がなかったので自作した

Pythonista 3

Pythonista 3

  • omz:software
  • 仕事効率化
  • ¥1,220

Pythonista のエディタには文字列検索機能はあるが、置換機能はない。変数や関数の命名が途中で気に入らなくなり、一括で新しい名前に変更したい時などは不便である。

幸い Pythonista は、アプリ内エディタの機能を拡張するための editorモジュールを備えており、ユーザーがエディタに好きなように機能を追加できる。

各 Pythonista ユーザーは置換機能を自作しているのかもしれないが、ググり足りないのかあまりそんな記事を見かけないため、自作した文字列置換機能を紹介することにした。

一括置換と、置換候補一覧を表示して置換対象を選択するものとで、2種類の方式を選択できるようにした。もっとうまく作れそうな気もするが、あまり作り込みすぎても他のスクリプト作成に割く時間がなくなるだけなので、当面はこれで凌ごうかと思う。

このスクリプトを表示している状態で、レンチボタンを押すと、ツールアイコンが並んだポップアップが出てくる。下の段はユーザーが勝手にツールを追加できるようになっており、"Edit"ボタンを押してアイコン追加のための"+"ボタンを押す。アイコン画像やアイコンのタイトル、起動時の引数などを設定できるようになっている。以下の画像はすでに"Replace"アイコンを追加した状態である。

これで、Pythonista 内のどのスクリプトを編集している時でも、レンチボタンからこの文字列置換スクリプトを呼び出せる。試しに、このスクリプト自体で文字列置換を試してみよう。

例えば、8行目と17行目の"choice", 32行目と36行目の"choice2"は、console.alert関数の戻り値を記憶するためのものだが、名前がイケていない気がする。何の選択肢に対する選択なのかが、いまいち良くわからない。

そこで、まず"choice"の方を、"use_selected"という変数名に変えたい。表示するアラートの問いは「選択中の文字列を変えたいか? 新しく文字列を指定するか?」というものだからだ(これでもまだイケていないというなら、またあとで変えれば良い)。

Pythonista 組み込みの検索機能は、幸いにして最初にヒットした文字列を自動で選択するようになっているから、これを取得できる。"Replace"アイコンを押して、次のダイアログを表示させる。

今は"choice"が選択されているから、「はい」を押して"choice"を置換の対象として指定する。

続いて、置換後の文字列を入力するためのダイアログが表示される。"use_selected"と入力して「指定」を押す。

次に表示されるダイアログで、以下のように「全置換」するか「置換文字列を選択」するかを選べるようにしてある。今は"choice"の方だけを"use_selected"に置き換えたいのであり、"choice2"は置換したくない。なので、後者の「置換文字列を選択」を選ぶ。

「置換文字列を選択」を選ぶと、スクリプト中の位置と置換候補の文字列 + 後ろの20文字をアイテムとするリストを表示する(iOS13でよく見かけるバグで、上の方が一部透けてしまっている)。

今回置換したいのは、最初の2つの項目。タップして選ぶと選択項目が暗くなるのでわかりやすい。"Done"を押すと置換処理を続行する。

置換が終わったら、終了報告をするダイアログが表示されて、これで終わり。

実際に確認してみると、"choice"の方は"use_selected"に置き換わっている一方、"choice2"は置き換わっていない。

「全置換」か「置換文字列を選択」を選ぶとき、「全置換」を選べば、そのまま全ての置換対象を置換する。以下の画像は、"choice2"を"replace_method"に全置換した図。

このように、Pythonista は Pythonista 自身を容易に拡張できるので、自分のカスタマイズ次第でどんどん使い勝手の良いプログラミング環境になる。

余談だが、最近リリースされた Pythonista 3.3 は、キーボードのカスタマイズもできるようになったようだ。いいアイディアが思いついたら何か作ってみたいと思う。

Pythonista 3 で Google 翻訳・改 GAS との連携

結局、前回の記事が最終回ではなくなったという。

今回の記事では、WebView を使わず appex モジュールを使用して共有シートから直接起動できるスクリプトを紹介する。

続編に至った経緯

以前、Pythonista 3 で Google 翻訳のページにアクセスするスクリプトを紹介した。

sutukeisu.hatenablog.com

しかし、iOS 13 へのアップグレードにより、Pythonista 3 とショートカットアプリの連携がうまくいかなくなってしまった。Pythonista 3 のスクリプトに引数が渡せなくなってしまったのだ。スクリプトは起動するが、明らかにサイトの URL を渡すことができなくなっていた。

拙い自作スクリプトではあったが、マルチウィンドウにできない iPhone ではそこそこ重宝していたので困ってしまった。

Pythonista フォーラムの iOS 13 についてのトピックでも、同様の報告が多数見られる。Pythonista 3 の更新は最近は停滞しているので、最悪未対応のままということも考えられる。

---- 追記(2020/04/18)----

その後、Pythonista は iOS 13 向けにアップデートされたが、ショートカットアプリで引数を渡せない不具合は残った。その代わり、URLスキームを以前より利用しやすくする改善が施された。その機能を使って、ショートカットアプリから引数を渡す方法を以下の記事で解説した。

sutukeisu.hatenablog.com

-- 追記終わり --

ということで、ショートカットアプリを使わない別の方法を考えた。

今回のアプローチ

前回までは共有シートからショートカットアプリを挟んで Pythonista 3 を起動する方法を取っていた。理由は、共有シートから直接 Pythonista 3 を起動すると、WebView を扱うスクリプトの場合、アプリ拡張に割り当てられたメモリ制限を超えてしまってアプリが落ちてしまったからである。しかし、WebView を用いない、手っ取り早い方法を思いつかなかったため、ショートカットアプリを頼った。

最初のスクリプト制作からしばらく経って、趣味人ながらも知識に蓄えができ、 WebView を扱わないスクリプト作成への解決への道筋がついた。それにより、新しいスクリプト制作に至った。

道筋とは具体的には次のようなものだ:

  • Google Apps Script(以下、GAS)で翻訳 API を作る
  • iOS のアプリで選択した文字列を appex モジュール経由で取得
  • 文字列を API に送り、翻訳結果を取得する
  • 得られた翻訳結果を表示

スクリプトの使用イメージ

上述の流れを実現すると以下の画像のような使用感になる。GAS による Web アプリの起動がやや遅いので少し待たされるが、アプリの遷移もなく翻訳結果を得ることが可能である。

f:id:sutukeisu:20191008230818g:plain

下準備

今回の肝は、以下の Qiita の記事で紹介されていた、Google 翻訳の Web API を GAS で自作する方法である(この記事がなければ今回のスクリプトは作らなかった)。

qiita.com

まずはこの Web API を自分のアカウントで同様に作成する。上記記事の作者のものを使うこともできるが、おそらく LanguageApp の使用回数に制限があると考えられるので、上記記事作者の使用回数を消費しないように、できるだけ自作することを推奨する。

GAS の始め方は以下の記事などが参考になる。

note.mu

ソースコード

上記の GAS のコードも含めて、コード全体は以下のようになっている:

The script to translate sentences by using the web ...

setting.json は、スクリプトの設定を JSON 形式で記述したファイルである。https://url.to.your.webapp の部分は自作 Web API の URL が入る。

UI 部品の解説

以下に translation2.pyui の編集画面を示した。また、画像の各 UI 部品に付した番号と、それに対応する部品名、Action に指定された関数名を表で示しておく(関数名がいい加減なのは許していただきたい)。その他の詳細は前掲の .pyui ファイル(JSON 形式)に記述されている。

(ボタンのサフィックスの順番がバラバラなのは、その順番に実装したから(汗))

番号 部品名 Action に指定された関数
button3 push_source
button1 switch_source_target
button4 push_target
label1 -
button6 add_lang
textview1 -
label2 -
button2 push_translate
button5 switch_tv_1_2
textview2 -

①と③は、それぞれ翻訳前と翻訳後の言語を指定するボタン。②は翻訳前と翻訳後の言語を入れ替えるボタン。⑤は扱う言語を追加するボタン(ロケールIDを追加する。詳しくは以下の記事が参考になる)。

[ ロケールID ] ja-jp と ja は違うの?どんな意味? ( LCID ) – 行け!偏差値40プログラマー

⑧は翻訳ボタン、⑨は上の TextView と下の TextView の内容を入れ替えるボタンである。

コードの解説

GUI 部品と関数の連携などは前回までの記事で解説したので、今回は主に前回までになかった要素を解説する。

appex モジュール

前掲の GIF 画像では、文字列を選択後、ポップアップメニューから「共有...」(英語版なら "Share...")→ "Run Pythonista Script" → 「Google 翻訳」と進んだ(「Google 翻訳」のアイコンは、"Run Pythonista Script" 画面の左上にある "Edit" ボタンからの操作で追加した)。

この方法でスクリプトを起動すると、appex モジュールを使って次のように選択した文字列を取得できる。

import appex

text = appex.get_text()

ちなみに、ポップアップメニューからではなくブラウザの共有ボタンからスクリプトを起動すると、選択された文字列の代わりに、その時開いている Web ページの URL が取得できる。

Web アプリへの URL の構築

自作 Web API への URLに続いて、?text=翻訳したい文字列&source=翻訳前言語&target=翻訳後言語 というクエリを追加することで、API に翻訳に必要な情報を渡している。urllib3 モジュールはこれをパイソニックに構築するには打ってつけである。

urllib3 モジュールはサードパーティ製。Pythonista 3 に pip コマンドでサードパーティ製モジュールをインストールするには、StaSh というツールを使う必要があるが、幸運にも urllib3 はデフォルトで Pythonista 3 にインストールされている。Pythonista 3 には、いくつかの便利なサードパーティ製モジュールがあらかじめインストールされていて、非常に便利である。

今回のコードでは、translate 関数でこのモジュールを使っている。以下にその部分の概略だけ抜き出す。

import urllib3

# PoolManagerの初期化。
# スクリプトではmain関数で行っている
http = urllib3.PoolManager()

def translate(翻訳したい文字列):
    # クエリをPythonの辞書で表現する
    query = {
        'text': 翻訳したい文字列,
        'source': 翻訳前言語,
        'target': 翻訳後言語,
    }
    # RequestMethodsのrequestメソッドでURLを構築し、HTTP RequestをWeb APIに送る
    # 戻り値のHTTPResponseで返ってきたResponseの情報が得られる
    r = http.request('GET', Web APIへのURL, fields=query)

    # ステータスコードが 200 OK なら
    if r.status == 200:
        # HTTPResponseのdata属性に翻訳結果が入っている
        # バイト列なので文字列にデコードする
        return r.data.decode('utf-8')
     else:
        # エラーコードならNoneを返す
        return None

urllib3.request.RequestMethodsrequest メソッドが便利なのは、Python 辞書からクエリを構築できることだけではない。翻訳したい文字列にアスキーコード以外の文字が入っている場合は、手動でクエリを作るならその文字をエスケープ処理しなくてはいけなくなるが、このメソッドはそこまで自動でエンコードしてくれる。

スクリプトを書いていた最初の頃は標準ライブラリの urllib を使おうとしていて、https が使えなかったり、エスケープ処理が念頭になく URL を作れなかったりしてハマったので、urllib3 を見て本当に便利に感じた。

InsecureRequestWarning を消す

urllib3公式ドキュメントによると、証明書の検証を有効にせずに HTTPS の URL でリクエストするとInsecureRequestWarningという警告が表示されるようである。今回は無視したかったので方法を探したら、以下の taratail の Q&A があった。今回はこの回答で示されたコードをそのまま使った。

teratail.com

ソフトウェアキーボードを非表示にする

.pyui ファイルの設定と、ボタンのイベントで呼ばれる関数の定義が終わったところで、最後に困ったのは TextView で文章を編集した場合、ソフトウェアキーボードを非表示にする手段がなく、翻訳結果がキーボードに隠れて見えなくなることであった。

Pythonista のドキュメントにはキーボード非表示について具体的な説明がない。仕方なく、Objective C / Swift による一般的な iOS 開発ではどうするのか調べると以下がヒットした。

blog.personal-factory.com

上記の記事の実装を行うことで、TextView 外の領域をタップするとキーボードが非表示になる。

Pythonista は objc_util というモジュールを持っており、Objective C の API を叩ける。原理的には同じ実装が可能なはずである。この記事をヒントに調査を続けると、Pythonista フォーラムでまさに同様の実装を解説している方がいた。

forum.omz-software.com

iOS 開発に詳しいわけではないので解説は避けるが、手短に対策を記すと以下のようになる。

まず、コード中に以下のようなカスタムビュークラスを定義する:

class MyView (ui.View):
    def __init__(self):
        pass

    def touch_began(self, touch):
        # Called when a touch begins.
        for subv in self.subviews:
            if isinstance(subv, ui.TextView):
                subvo = ObjCInstance(subv)
                if subvo.isFirstResponder():
                    subvo.resignFirstResponder()

それから、.pyui ファイルを編集する。レイアウトの外側の空白の領域をタップすると、UI 部品が無選択になる。ここで i ボタンを押すと、基底の View のインスペクタが表示される。その一番下に "Custom View Class" という項目があるので、"MyView" を指定する。これにより、基底の View にはデフォルトの View ではなく新たに定義した MyView が使われるようになる。

以上の対策で、GIF 画像のように最後にソフトウェアキーボードを非表示にすることができる。

終わりに

前回までの記事とは異なり色々と端折ったが、想定以上に長い記事になってしまった。

趣味で Flask を練習していたこともあり、Web プログラミングに抵抗がなくなってきたところで、ショートカットアプリ周りのアクシデントに見舞われたので、タイミング的にはちょうどよかったとも言える。今回のスクリプト作成を通して、改めて Pythonista 3 のもつ柔軟性を感じた。願わくば開発継続されてほしい。

Pythonista の更新が滞っていることは冒頭で触れた。フォーラムでは作者に寄付をしようかという動きもあったが、「アプリの販売実績が良いので寄付は必要ないのでは」という意見もあった。しばらくの間はとりあえず、引き続き使える機能だけを使ってうまく対応する他はないようだ。

(余談:iOS 13 で Pythonista 3 からショートカットを作成すると、なぜかそのショートカットからの起動ではスクリプトが二重起動してしまうという不具合が生じてしまっている。こちらはむしろショートカットアプリを使うとうまく対応できる。引数が必要ない限りはショートカットアプリから Pythonista スクリプトを普通に起動できるからである。)

Pythonista 3 で Google 翻訳を使いやすく (3) iOS ショートカットアプリとの連携

前回から随分間が空いてしまった。前回でおおよそ主要機能は完成していた(全てのコードは前々回の記事に記載)。

sutukeisu.hatenablog.com

sutukeisu.hatenablog.com

今回は Pythonista 3 と iOS のショートカットアプリの連携、そしてアラート表示について触れていきたい。しばらく放っておいてしまったこのシリーズもこの記事で完結となる。

---- 追記:2020/04/18 ----

iOS 13では不具合によりこの記事の方法でスクリプトに引数を渡すことはできない。URLスキームを用いた方法でこの記事の目的を実現できるので、新しくその方法についての記事を書いた。現在はそちらを参照されたい。

sutukeisu.hatenablog.com

また、この不具合への対処方法がなかった時期に、ショートカットアプリを経由せずにPythonistaのappexモジュールとGoogle Apps Scriptを使って翻訳結果を取得するスクリプトも作成した。そちらの記事もある。

sutukeisu.hatenablog.com

---- 追記終わり ----

なぜ "Run Pythonista Script" を使わないのか

Pythonista の使い方をご存知の方は、iOS の共有シートから "Run Pythonista Script" のボタンを介して起動すれば良いのではと思われるかもしれない。

f:id:sutukeisu:20190907210654j:plain:w250

実際、最初はそうしたかった。アプリ内の "URL to QR Code" 等の Example を参考にすれば、Web ブラウザからの URL の取得も容易なことがお分かりいただけると思う。

しかし、問題が1つあった。それは、共有シートから Pythonista を起動するとき、通常のアプリ起動よりもメモリの割り当てが少ないことである。WebView を同時に2つ保持している拙作のスクリプトは、このメモリ制限を超えてしまうらしく、起動してもすぐに落ちてしまった。

そこで、App Extension としてではなく Pythonista を直接起動するという方法を取らなければならない。このときこれを簡単に実現する手段として、Apple 公式のショートカットアプリを利用する方法を今回は採用した。

ショートカット

ショートカット

  • Apple
  • 仕事効率化
  • 無料

ショートカットを作成する

まずはショートカットアプリを開き、ライブラリ画面の「ショートカットを作成」を選択する。ショートカットの編集画面が開くので、検索欄に「テキスト」と打ち込むと、テキストの処理に関連するアクションがたくさんヒットする。この中で、単に「テキスト」と書いてあるアクションを選択する。

f:id:sutukeisu:20190907214107j:plain:w250

このアクションには「テキストを入力...」という欄がある。ここには「ショートカットの入力」を入れる(キーボードの予測変換領域に表示されている)。これにより、このショートカットの呼び出し元から入力を取ることができる。

次に、検索欄に「Pythonista」と打ち込む。Pythonista 関係のアクションは「スクリプトを編集」と「スクリプトを実行」の2つある。今回は後者を使う。

ここで、説明文を読む(なぜか親切にも日本語。なぜアプリ自体とドキュメントは英語だけなのか……)。すると、どうやらスクリプトに変数を渡すことができ、スクリプト側ではそれをsys.argvで受け取れるという。とても馴染みのあるアクセスの仕方ができる。

f:id:sutukeisu:20190907214307p:plain:w250 f:id:sutukeisu:20190907214353j:plain:w250

スクリプトを実行」アクションには「スクリプト」の欄がある。ここには、スクリプトのパスを、Pythonista のルートディレクトリからの相対パスで入力すれば良い。拡張子は必要ない(筆者の場合は "MyTools/TranslateSet/translate_set" と入力している)。

使うアクションはこの2つ全てである。あとは右上の設定アイコン(スイッチのオンオフが交互になっているマーク)を押して、好きな名前とアイコンを決める。そして今回の場合、「共有シートに表示」をオンにする。すると「受け入れの種類」という項目が新しく生えるので、これを開く。デフォルトではどんな種類の入力も扱えるようになっているが、今回は URL のみを扱うだけなので、チェックは URL のみに入れる。

これで、ショートカットアプリ側での設定は終了である。ショートカットの内容が下の画像のようになっていれば良い(URL のみを受け入れる設定にしたので、「このショートカットはURLを受け入れます」と表示されている)。

f:id:sutukeisu:20190907220717p:plain:w250

あとはスクリプト側で、受け取った URL を使う。

アラートの表示

前回の最後のスクリプトに手を加える。

Pytonista の console モジュールに alert 関数がある。以下の画像のように選択肢を表示できる。

f:id:sutukeisu:20190907221738p:plain:w250

公式ドキュメントによると、引数は以下のようになっている。

console.alert(title[, message, button1, button2, button3, hide_cancel_button=False])

なので、タイトル、説明文、最大3つまでのボタンのテキストを引数として渡せば良い。戻り値は、button1が選択されれば1button2が選択されれば2button3が選択されれば3である。Cancelボタンを押したときは戻り値が返らず、KeyboardInterrupt となってスクリプトが終了する。

今回のコード

sys.argvは通常通り、第1要素はスクリプトのファイル名で、第2要素以降が引数である。Pythonista で直接実行したか、ショートカット経由で起動して引数に URL を持っているかで条件分岐している。そして、受け取った URL のサイトを表示するか、あらかじめようしてある選択肢(Google のトップページか、Pythonista のドキュメント)を表示するかで、ui.WebView.load_url()に渡す URL を変更している。自分で使うことだけを念頭に書いたので、かなり付け焼き刃なコードになってしまっているのは申し訳ない。

import ui
# 以下の2つの import を加える
import console
import sys

# ...
# 中略
# ...

webview1 = v['webview1']
webview1.delegate = MyWebViewDelegate()

# ここから前回のコードとの差し替え
# 元のコードは以下の通り:
# webview1.load_url('http://omz-software.com/pythonista/docs/')
# textfield.text = 'http://omz-software.com/pythonista/docs/'
text = ''
if len(sys.argv) > 1:
    text = sys.argv[1]
if text.startswith('http://') or text.startswith('https://'):
    result = console.alert('URLを開く', '取得済みのURLがあります。開きますか?', 'はい', 'Googleを開く', 'Pythonista Docs を開く')
    if result == 2:
        webview1.load_url('https://www.google.co.jp/')
        textfield.text = 'https://www.google.co.jp/'
    elif result == 3:
        webview1.load_url('http://omz-software.com/pythonista/docs/')
        textfield.text = 'http://omz-software.com/pythonista/docs/'
    else:
        webview1.load_url(text)
        textfield.text = text
else:
    result = console.alert('URLを選択', 'どちらを開きますか?', 'Googleを開く', 'Pythonista Docs を開く')
    if result == 1:
        webview1.load_url('https://www.google.co.jp/')
        textfield.text = 'https://www.google.co.jp/'
    else:
        webview1.load_url('http://omz-software.com/pythonista/docs/')
        textfield.text = 'http://omz-software.com/pythonista/docs/'
# 差し替えはここまで

webview2 = v['webview2']
webview2.load_url('https://translate.google.co.jp/?hl=ja&tab=wT')
webview2.alpha = 0.0

webview1.bring_to_front()

v.present('sheet')

これで全ての作業は終了である。第1回の記事に掲載した GIF 画像のような流れがこれで実現する。

終わりに

本当はもっと早くこの記事のシリーズを終わらせる予定だったが、体調がままならなかったりして遅れに遅れてしまった。終わらせることができてよかった。

今でも Pythonista を生活に役立てる試みを続けている。暗記カードもどきのようなものを作って英単語の暗記に使ったり、numpy や matplotlib がデフォルトで入っていることをいいことに、統計の計算に使ったりしている。いつでもどこでも気軽に使用できるスマートフォンという環境でプログラミングを行えるということは素晴らしい。Pythonista は流石に重量級の用途には向いていないが、ちょっとした短いスクリプトでも十分役に立つと感じられる。

これからもちょっとずつ、楽しく役に立つコードを考えていきたいと思う。

Pythonista 3 で Google 翻訳を使いやすく (2) ロジック部分

前回の記事では、UI の準備まで行った。

sutukeisu.hatenablog.com

今回はアプリケーションのロジック部分の作成と、UI との結合について解説する。なお、GitHub Gist にアップロードした全コードは前回示したので、今回は省略する。

.pyui ファイルのロード・UI の表示

UI ファイルの名前が translate_set.pyui で、かつ Python スクリプトと同じディレクトリにある場合、最低限次のように記述すれば .pyui ファイルの読み込み・UI の表示が行われる(なお、最後の行の ui.View.present 関数の引数 'sheet'iPad の場合に意味のあるものだが、iPhone 版でこのように記述しても実行自体はうまく行く)。

import ui

v = ui.load_view('translate_set')
v.present('sheet')

ここまで問題なく実行できることを確認した上で、次のステップに進む。

ウィジェットへのアクセス方法

今表示される画面では、WebView が真っ白なので、何か Web サイトを表示させてみよう。

先ほどの ui.load_view 関数で取得した ui.View オブジェクトを通して、.pyui ファイルにある全てのウィジェットにアクセスできる。方法はかなり簡単で、先ほどのスクリプトであれば v['widget_name'] と辞書のようにウィジェット名をキーにしてウィジェットオブジェクトを参照できる。 v['webview1'] として webview1 を参照し、変数 webview1 でアクセスできるようにする。Pythonista の WebView のドキュメント を読むと、load_url メソッドで指定の URL 先を表示できることがわかるので、それを使う。さらに、今手前にある WebView は webview2 の方なので、webview1 を手前に出すために bring_to_front メソッドを呼ぶ。

import ui

v = ui.load_view('translate_set')
webview1 = v['webview1']
webview1.load_url('http://omz-software.com/pythonista/docs/')
webview1.bring_to_front()
v.present()

これで Pythonista のドキュメントが表示されるようになった。

ウィジェットに関数をひもづける

ここまでの状態でも、リンクを踏めばそのリンク先を表示できる。

ここからさらにブラウザ的な使い勝手にするためには、進むボタンと戻るボタンにそれぞれ ui.WebViewgo_back メソッド、go_forward メソッドをひもづけたい。 まずは、ボタンから呼び出すための関数を次のように書く。

def page_back(sender):
    webview = sender.superview['webview1']
    webview.go_back()

def page_forward(sender):
    webview = sender.superview['webview1']
    webview.go_forward()

ここで、それぞれの関数の引数 sender には、ボタン操作によって関数が呼ばれた時に、呼び出し元のボタンへの参照が入る。sender を通してアクセスしたいのはボタンではなく WebView なので、一旦ボタンが所属している上の階層の View を参照するために superview 属性を呼び出し、先ほどまでと同じように辞書と同じ方式で webview1 をキーとして webview1 を取得する。あとは go_back, go_forward メソッドをそれぞれ使用する。

最後に、一旦 Python スクリプトを離れて .pyui ファイルに戻る。button2(戻るボタン)と button3(進むボタン)それぞれの Action 項目に、ひもづけたい関数の名前を書けば、ボタンを押した時にそれぞれの関数が実行されるようになる。

WebView の Delegate を設定する

今回のスクリプトでは、先ほどの要領で button1(虫眼鏡ボタン)に jump_to_link 関数、segmentedcontrol1(「サイトビュー | Google翻訳」切替ボタン)に switch_webview 関数をひもづけていけば、Pythonista 内で完結したアプリケーションの動作は大体できたことになる。webview1 に任意の Webサイト、webview2 に Google 翻訳のページを表示すれば、切替ボタンで両者間を素早く行き来できる。

残ったのは、webview1 で新しい URL 先を表示した時に、連動して textfield1 にそのサイトの URL を表示させる機能である。これには、WebView オブジェクトが持っている delegate 属性に、自分で定義した delegate オブジェクトを渡す方法が使える。ただし、多少ややこしい問題もあった。

そのややこしい問題はひとまず置いておいて、まとめも兼ねて、ここまでの内容分のスクリプトを全て書き出してみた。

import ui


user_action = True


class MyWebViewDelegate(object):
    def webview_should_start_load(self, webview, url, nav_type):
        global user_action
        if nav_type == 'link_clicked' or user_action:
            textfield = webview.superview['textfield1']
            textfield.text = url
            user_action = False
        return True

    def webview_did_start_load(self, webview):
        pass

    def webview_did_finish_load(self, webview):
        pass

    def webview_did_fail_load(self, webview, error_code, error_msg):
        pass

def page_back(sender):
    global user_action
    user_action = True
    webview = sender.superview['webview1']
    webview.go_back()

def page_forward(sender):
    global user_action
    user_action = True
    webview = sender.superview['webview1']
    webview.go_forward()

def jump_to_link(sender):
    global user_action
    user_action = True
    textfield = sender.superview['textfield1']
    if textfield.text:
        webview = sender.superview['webview1']
        webview.load_url(textfield.text)

def switch_webview(sender):
    idx = sender.selected_index
    webview1 = sender.superview['webview1']
    webview2 = sender.superview['webview2']
    if idx == 0:
        webview2.alpha = 0.0
        webview1.alpha = 1.0
        webview1.bring_to_front()
    else:
        webview1.alpha = 0.0
        webview2.alpha = 1.0
        webview2.bring_to_front()

v = ui.load_view('translate_set')

textfield = v['textfield1']
textfield.clear_button_mode = 'unless_editing'

webview1 = v['webview1']
webview1.delegate = MyWebViewDelegate()
webview1.load_url('http://omz-software.com/pythonista/docs/')
textfield.text = 'http://omz-software.com/pythonista/docs/'

webview2 = v['webview2']
webview2.load_url('https://translate.google.co.jp/?hl=ja&tab=wT')
webview2.alpha = 0.0

webview1.bring_to_front()

v.present('sheet')

delegate オブジェクトのテンプレートは、先ほどの WebView のドキュメントに記載されている。webview_should_start_load メソッドはページを読み込み始める時に呼ばれるメソッドで、引数にはそれぞれ、この delegate を持っている WebView オブジェクト、新しく読み込まれる URL、ページの読み込みが始まった原因を示すナビゲーションタイプが渡される。ナビゲーションタイプについては今回初めて知ったが、おそらく本家 Apple の WebKit のドキュメントにあったこれの記載に対応していると思われる。ただしこの nav_type に渡される文字列は、その記載と若干異なるものが渡されるので、print デバッグであらかじめ渡される文字列を確認してから使用した。

nav_type'link_clicked' のときと、自前のグローバル変数 user_action が True のときに限り、textfield1 に URL を反映させることにした。user_action 変数は、ボタン操作の時に呼び出される関数内で True にしておき、直後のページ読み込みで URL を textfield1 に反映させるためのものである。それが終わったらすかさず False にしている。

それ以外のケースではそのページのアドレス以外のURL(SNSボタンに関連するものなど)が読み込まれていることが多く、反映された URL が意味をなさないため、これらのケースは除外することにした。この nav_type まわりの処理をどうするかが一番ややこしかった。もっとうまい方法もあるのかもしれない。

ここまでで、基本的な機能が概ね完成した。スクリプト起動時のコンソールアラート表示や、iOS のショートカットアプリを経由する方法などがまだ残っているが、例によってこれらは次回説明することにする。