RubyCocoaを使ってQuicksilverプラグインを書く

RubyCocoaを使って、Quicksilverのプラグインを書いてみました。 受けとった文字列に、”hello world” をくっつけるだけのものです。

プロジェクト一式のコードは、CodeReposに。 ライセンスは修正BSDで。 RubyCocoaがインストールされていれば、TigerでもLeopardでも動くと思います。

仕組み

RubyCocoaでQuartzComposer CustomPatchのときに書いたように、 CocoaアプリじゃなくてプラグインをRubyCocoaで書くには、RBBundleInit() という関数をプラグインの初期化の際に呼んであげればよいわけです。 ところが、Quicksilverプラグインの初期化ってどこなんだろうという。

QSActionProviderを継承したクラスで実際のプラグインの動作 (Action) を記述するので、このクラスのinit()で RBBundleInit() を呼べばいいかなーと考えてやってみたところ、Quicksilverをクラッシュさせてしまいます。こまった。

しかたないので、Actionを記述する performActionOnObject() の中で、RBBundleInit()を最初の一度だけ呼ぶようにしました。その中で、Objective-CからRubyクラスのインスタンスを参照できるようにしておきます。

あとは、RubyのクラスでActionを実装し、Objective-CからRubyのメソッドを呼んで移譲します。これでできあがり。 かなり強引で美しくないのですが目的は果たせるかな、というダーティハックです。

コード片

Objective-CのActionProvider:

 (QSObject *)performActionOnObject:(QSObject *)dObject{
  // initialize RubyCocoa
  static bool loaded = false;
  if (!loaded) {
    if (RBBundleInit("qs_action.rb", [self class], self)) {
      NSLog(@"[RubyCocoaPluginAction.performActionOnObject] RBBundleInit failed"
);
      abort();
    }
    loaded = true;
  }
 
  // delegate actual action to Ruby class
  QSObject *ret = [QSObject objectWithString:[rb_ act:dObject]];
  return ret;
}

RubyCocoa側:

class Action
  def initialize(logger)
    @logger = logger
  end
 
  # write something great :)
  #  - arg : QSObject
  def act(arg)
    val = arg.stringValue
    @logger.info(val)
    'Hello world, ' + val
  end
end # Action
 
require 'osx/cocoa'
OSX.init_for_bundle do |bdl, owner, log|
  # bdl    - the bundle related with the 2nd argument of RBBundleInit
  # owner  - the 3rd argument of RBBundleInit as optional data
  # log    - logger for this block
 
  act = Action.new(log)
  owner.setInstance act
end

あと、やるとしたら

  • もうちょっときれいに初期化する (何らかのクラスのコンストラクタが望ましい)
  • QSActionProviderをRubyのクラスで継承する (よりRubyだけで書けるように)
  • ns_importとかつかってみる?

「RubyCocoaを使って」 よりも、「RubyCocoa」、スマートにプラグインを書けるようにしたいものです。 まぁ、まずは第一歩ということでひとつ。

まとめ

RubyCocoaを使って、Quicksilverプラグインをつくるためのとっかかりについて書きました。 RubyCocoaでバンドルを書くときに悩むのは初期化のタイミングだと思います。 今回はここを無理矢理解決しました。 Objective-Cを学んでる時間があったら、慣れ親しんでるRubyでちゃちゃっと コード書きたいよ! という方の参考になればと思います。

参考


2008.02.03 追記

RubyCocoaのhisaさんにコメントいただき (!)、もっとよい方法を教えてもらいました。 こちらについても、まとめて別エントリにします。

  • hisa

    コメント内のソースがぐちゃぐちゃになってたのを直してくれたんですね。どうもありがとう


    僕のやつは、ヤフー翻訳を呼び出すアクションを作るのにhpricotが使いたかったのでRubyCocoaをリンクしたみたいな感じになってますが、Rubyでアクション(やその他)を書ける汎用のプラグインがあるとうれしいですよね。

  • hisaさん:


    おおおお、ありがとうございます!
    つくってらしたんですね。
    QSActionProviderの派生クラスをRubyで実装するのは、まさにやりたかったところでした。


    頂いたプロジェクトソースを元にして、自分なりのまとめを書いてみます。

  • hisa

    おおすばらしい!


    実を言うと僕も作って使ってたのですが、解決できていない問題もあったりして、無精して公開してませんでした。メールでソース(プロジェクト)を送るつもりですので、よろしければ煮て焼いて食うなりなんなりしてください。


    初期化のタイミングですが、僕はプラグインのクラスがロードされたタイミングでやってます:


    @implementation QSRubyEnablerPlugin
    + (void)load {
    static int installed = 0;
    if (! installed)
    if (! RBBundleInit("init_ruby_enabler.rb", [self class], self))
    installed = 1;
    }
    @end

    それからアクションは QSActionProvider の派生クラスとして実装しました。


    module QSRubyEnablerPluginActionModule
    def obj2qsobj(obj)
    obj = OSX::QSObject.objectWithString(obj.to_s) unless obj.is_a? OSX::QSObject
    obj
    end

    def show_large_type(obj)
    OSX::QSTextActions.provider.showLargeType(obj2qsobj(obj))
    end

    def paste_string(str)
    pb = OSX::NSPasteboard.generalPasteboard
    pb.declareTypes_owner([OSX::NSStringPboardType], self)
    pb.setString_forType(str, OSX::NSStringPboardType)
    end

    end

    class TranslatorAction < OSX::QSActionProvider
    include OSX
    include QSRubyEnablerPluginActionModule
    require 'yahoo-honyaku'

    def initialize
    @yahoo_honyaku = YahooHonyaku.new
    end

    def translateEJ(obj) translate_qsobject(:ej, obj) end
    def translateJE(obj) translate_qsobject(:je, obj) end
    def translateCJ(obj) translate_qsobject(:cj, obj) end
    def translateJC(obj) translate_qsobject(:jc, obj) end
    def translateKJ(obj) translate_qsobject(:kj, obj) end
    def translateJK(obj) translate_qsobject(:jk, obj) end

    private
    def translate_qsobject(mode, obj)
    src = obj.stringValue
    src.gsub!
    result = @yahoo_honyaku.translate(mode, src.to_s)
    show_large_type(result)
    paste_string(result.to_s)
    nil
    rescue Exception => err
    show_large_type(err.message)
    NSLog("QSRubyEnablerPluginAction error: %@", err.message)
    nil
    end

    end
blog comments powered by Disqus