きりかノート 3冊め

おあそびプログラミング

発表:Macのサービスメニュー

7/24のCocoa勉強会の発表内容とか。

発表内容

Twitterでクライアントアプリを使うとURL短縮サービスが使えるけれど、ブラウザだと自分で短縮してコピペする必要がある→めんどう

今回つくってみたアプリはサービスメニューで短縮できるもの。

テキストを選択してサービスメニューをえらぶと

URLのところが短縮URLに置き換えされるよ。

サービスメニューは他のアプリケーションに操作をさせることができる。例としては、

  • 選択したテキストの辞書を引く
  • 選択したテキストを要約する
  • スクリーンショットを取得して、今のアプリに貼り付け

などがある。簡易アプリケーション連携みたいな…?

実行方法は

  • アプリケーションメニューの「サービス」から
  • コンテキストメニューから(対応しているアプリ・Viewのみ)
  • ショートカットキー(割り当てされていれば)

がある。いつのまにかコンテキストメニューが!

Snow Leopardになって、"System Serivce"から"Services"に名前が変わった。ますますどうやって検索すればよいのだろーか。勘弁してほしい。

Xcodeでも使われてるAppleScriptメニューや、クリップボードユーティリティのアプリなど、似たような機能を提供するものもあるけれど、さまざまなアプリがサービス機能を提供して、それを統合的にアクセスできるのがサービスメニューの良いところ。

いつのまにか環境設定パネルが導入された(キーボード > サービス)。利用するサービスメニューのオン・オフやショートカットキーの設定ができる。これは以前にはなかったもの(教えてもらったことによるとLeopardもなかった→確認した)で、ほんとうにすごく使いやすくなった。

Leopardのサービスメニュー。今見ると、アプリケーションごとにグループされてるとか、サービス対応アプリがぜんぶ表示されてるとか、いろいろありえない。

サービスメニューの実装や仕組みについてはリファレンスのServices Implementation Guideにぜんぶ書いてある。

サービスはクリップボードを受け渡しすることで実装されている。サービスを提供するアプリ側としては、クリップボードを読み込んで、加工して、クリップボードに書き戻すという処理になる。

相手のアプリに渡すだけ、相手に渡して加工結果を受け取る、相手からもらうだけ、の3パターン。ドキュメントに2通りとあるのは謎。

(注:要約する、は実際には戻してなかった。ゴメンナサイ)

サービスメニューを実装するにはリファレンスにしたがって作業していけばよい。

Info.plistはこんなふうにNSServicesをつくって、そこにメニューの設定をする。

     :
   <key>NSServices</key>
   <array>
     <dict>
       <key>NSMessage</key>
       <string>shortenWithTinyURL</string>
       <key>NSMenuItem</key>
       <dict>
         <key>default</key>
         <string>Shorten URL via TinyURL</string>
       </dict>
       <key>NSPortName</key>
       <string>MyShortening</string>
       <key>NSSendTypes</key>
       <array>
         <string>NSStringPboardType</string>
       </array>
       <key>NSReturnTypes</key>
       <array>
         <string>NSStringPboardType</string>
       </array>
     </dict>
   </array>
     :

サービスの機能を実装したメソッドを書く。メソッドの名前は"(Info.plisaで指定したNSMessageの値):userData:error:"になる。内容はペーストボード読んで、加工して、書き込みという流れになる。

 - (void)shortenWithTinyURL:(NSPasteboard *)pboard
                   userData:(NSString *)userData error:(NSError **)error {
       :
     text = [pboard stringForType:NSPasteboardTypeString];
       :
     [pboard clearContents];
     [pboard writeObjects:[NSArray arrayWithObject:replaceText]];
 }

アプリ起動時に、サービスのリクエストに対応するオブジェクトを登録する。今回は単純でクラス1個しかつくらなかったので、app delegateをそのまま使った。

 - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
     [NSApp setServicesProvider:self];
 }

気をつける点としては、ログアウト - ログオンしないとメニューが現れないことがある。(サンプル開発中にこれに気付かずにけっこー時間をムダにしてしまった…)強制的に再認識させる関数NSUpdateDynamicServicesもあるのでそれを使う手もあり。

あと、サービスをリクエストしてから返ってくるまで元のアプリは待ちになっちゃうので、時間のかかる操作をさせるのは向かない。すぐに返ってくる自動処理的なものがサービスにするのに向いている。相手のアプリに渡してからユーザ操作があるようなものはやめといたほうがいい。(かなり前にEditCastをサービスで実装してみて、しょんぼりしたことがある)

今まで話したように、Snow Leopardでマジ便利になった。みんな見直してあげるべき。

そういえばiOSだとどうなんだろ?→iPhoneでのサービス的なものの提案をしている人を見つけた! A Services Menu for iPhone (こんなふうならどう?という動画と説明)

おしまい。

あとがき的な

URL短縮処理の本体はRubyの外部プログラム(コマンド)にした。テキスト処理やネットワークというRubyの得意分野が中心だものね。というか、RubyCocoaで一度つくったのだけど無関係のアプリがCPUを食いまくるというなぞの現象が発生したので書き直し。でもURL短縮処理はRubyのまま。

これObjective-Cで書くのはこの量じゃ済まないよね、きっと。

 def run(str)
   $stdout.write(shorten(str) {|url| tinyurl(url)})
 end

 # shorten methods
 def shorten(str)
   ret = str.gsub(URI.regexp(%w{http https})) {|url|
     if url.length <= MIN_LENGTH then
       url # do not need to shorten
     else
       yield url
     end
   }
   return ret
 end

 # see http://ja.doukaku.org/comment/6837/
 def tinyurl(url)
   begin
     ret = open("http://tinyurl.com/api-create.php?url=#{URI.escape(url)}").read
     # validate return value
     unless %r!\Ahttp://tinyurl.com/! =~ ret then
       ret = url
     end
   rescue
     ret = url # TinyURL failure, such as offline -> return input string
   end
   return ret
 end

本当は複数のURL短縮サービスを利用できるように実装したかったのだけど、間に合わなかったり、APIキーが必要なものがほとんどだったりで見送り。

あと、サービスを利用する側についてはリファレンスの"Using Services"に書いてある。利用できるペーストボードの内容タイプの指定や、メニューの利用可否のvalidateなど。

これは発表の最中にも話したけれど、プログラムからサービスを呼ぶことができる。以前のWebKitではコンテキストメニューの「Googleで検索」がサービスの呼び出しを利用することで実装されていた。