2012年2月16日木曜日

Mac App Store の審査に通らなかったアプリケーション CLCL を Gumroad で販売開始!

CLCL という Mac 用アプリケーションを Gumroad というサーヴィスを通して販売を始めました。 販売を開始したのが2月14日で、おそらくその時点で Gumroad でアプリケーションの販売を開始した世界初だったように思う。 無料の Lite 版は Mac App Store に出ているのでそちらもよろしくお願いします。
 以下、経緯。
 今月はじめ、CLCL という Mac 用ラウンチャアプリケーションを Mac App Store の審査に出した。
 CLCL は修飾キィを二度押しあるいは三度押しすることでアプリケーションを起動させるラウンチャアプリケーションなんやけど、Lite 版との差異がなさすぎるというような理由で Reject されてしまった。Command と Option に機能登録できるのが Lite 版、それに加えて Shift、Control、Fn にも登録できるのが通常版で、違いはそれだけ。いまのところ、通常版にそれ以上の機能差異を実装するアイディアがなかったけん、通常版はお蔵入りになってしまっていた。

 そこに登場したのが Gumroad。詳しくは @fladdict さんの記事「誰でもデータを直販できるGumroad入門。クリエイターの生活は変わる?」を参照。かいつまんで言えば、何らかのデータ販売を超かんたんに行えるようにするサーヴィスである。

 Gumroad を使えば App Store の審査や、審査によるリリースまでのタイムラグがないけん、めんどうがなくていい。運営側のとる手数料も販売価格の 5% + $0.30 と App Store の 30% に比べて安い。一方、露出は販売者のマーケティング力に依存するけん、私のような超弱小はキツいことになる。
 ともあれ、Gumroad がなければいまのところ販売できることもなくお蔵入りになってしまっていたアプリケーションを出せたのは大きい。

 現在把握している限りで Gumroad でアプリケーションを販売しているのが私の他に二例。 いっしょに頑張りましょう!

2012年1月29日日曜日

既存のものと同名のネームスペースを addNamespace すると、Snow Leopard 環境で Segmentation fault になる

NSXMLElementaddNamespace メソッドにおいて、レシーヴァがすでに同名のネームスペースを持っている場合、かつ Snow Leopard な環境で実行すると Segmentation fault で落ちる。

 以下のような HTML があるとする。 html 要素に epub という名前のネームスペースを持っている。

 次に、この HTML を読み込んで処理を行う以下のようなコードがあるとする。 test.html にはすでに epub というネームスペースを持っているが、そこにさらに同名のネームスペースを追加する処理を行う。このコードは Lion では正常に動作するが、Snow Leopard で実行すると addNamespace: の部分で Segmentation fault で落ちる。

 addNamespace: のドキュメントによると、
If the receiver already has a namespace with the same name, aNamespace is not added.
とのことで「レシーヴァがすでに同名のものを持っている場合はネームスペースは追加されない」、つまり何もしないということだと思うんだが、そして Lion ではそのようになっているんだが、Snow Leopard では Segmentation fault で落ちる。うーん……。

 したがって、Snow Leopard で実行される場合は addNamespace: する前に同名のネームスペースがあるかどうかをチェックしておく必要がある。ある名前を持つネームスペースがあるかどうかは namespaceForPrefix: メソッドで調べることができる。

2011年12月31日土曜日

Cocoa テキストシステムのキィバインドをさらに Emacs に近づける

本ブログのエントリィ「眠れぬ夜のために: KeyBindingsEditor」の六年ぶりの続編。

 上記エントリィの繰り返しになるけど、Mac OS X の Cocoa フレームワークを用いて作られたアプリケーションのテキストフィールドは、ディフォルトで Emacs 風なキィバインドが割り当てられている。たとえば Control + p/n/f/b/a/e によるカーソル移動や Control + d/h によるカーソル前後の文字削除、Control + k/y による kill-lineyank など。
 Emacs 風のキィバインドに慣れたユーザであれば、両手をキィボードのホームポジションに置いたままで様々な操作ができるけん、テキスト編集のスピードが上がる。みんな覚えよう!

 ほなけど、ディフォルトの割り当てだけではまだまだ Emacs でよく使う操作には足りない。今回は Cocoa テキストシステムのキィバインドをさらに Emacs に近づけることを考える。

 今回は以下の操作をキィバインドに割り当てる。
  • set-mark
  • newline
  • newline-and-indent
  • kill-region
  • kill-ring-save
 キィバインドの割り当てには前回のエントリィと同じ KeyBindingsEditor を用いる。
  1. KeyBindingsEditor を立ち上げる
  2. メニューバーから File -> Open -> User Key Bindings を選択してユーザのキィバインド定義ファイルを開く
  3. Add Binding ボタン(2 ストローク以上のキィバインドの場合は Add Multikey binding ボタン)を押してキィバインドを追加する
  4. 追加したキィバインドを選択して、ウィンドウ下部の Keystroke 欄をクリックし、好みのキィバインドをタイプする
  5. Actions 欄でキィバインドに割り当てる操作を選択する。ひとつのキィバインドにふたつ以上の操作を割り当てたい場合は Add Action ボタンを押してアクションを増やす
  6. 3 - 5 を必要なだけ繰り返す
 KeyBindingsEditor でキィバインドを割り当てた結果が以下の画像のようになる。
 画像中で Action 欄が ( となっているのはふたつ以上のアクションを割り当てたもの。具体的には Control + j には insertNewline:indent:Control + [ に続く 2 ストローク目の Control + w には deleteToMark:yank: が割り当てられている。

 今回の KeyBindingsEditor での割り当てで kill-ring-save に対応するキィバインドは Control + [Control + w とした。Emacs において kill-ring-save に対応するキィバインドは M-w であり、ふだん私が Emacs でこの操作を行う場合は MetaC-[ で代用してから 2 ストローク目で w を単独で押す。本来であれば KeyBindingsEditor での割り当ても 2 ストローク目は単独の w で構わない。ほなけど、日本語入力が ON になっている状態で 2 ストローク目の w を押すと、その入力が IM に取られて割り当てた操作が実行されない(日本語入力が OFF の場合は単独の wで動作する)。Control を修飾すると日本語入力が ON の状態でも動作するので、今回のようにしている。

 これで 一般的なCocoa アプリケーションのテキストフィールドにて割り当てた操作を実行できる(Cocoa アプリケーションではなかったり、テキストフィールドを独自実装しているような場合は実行できない。たとえば Google Chrome なんかは動作が怪しい)。たとえば Control + Space でマークし、カーソルを動かして Control + w すると、マークした箇所からカーソル位置までの文字を削除して kill-ring に格納することができる。kill-ring の内容は Control + yyank。つまり、ホームポジションのままでカットアンドペーストができることになる。
 割り当て変更後にはアプリケーションの終了、再実行が必要となる。

 ここで、kill-ring の内容と Mac OS X 標準のペーストボードの内容とは関係がないことに注意。コンテキストメニューや Command + c/x からコピィ・カットした文字を Control + yyank することはできないし、上述の Control + w で kill-ring に格納した文字を Command + v でペーストすることはできない。それぞれが独立して動作する。

2011年12月20日火曜日

Mac アプリケーションの環境設定ウィンドウを作る

環境設定ウィンドウについて

 Mac アプリケーションの動作等を設定するときに用いる環境設定ウィンドウを作る。

 環境設定ウィンドウの特徴は、
  • クラスは NSPanel (非 NSWindow
    これはエスケープキィを押したときにウィンドウが閉じられることから
  • ツールバーを持ち、そのツールバーアイテムをクリックすることでウィンドウのヴュウを切り替える
  • ヴュウを切り替える際にウィンドウの大きさがアニメーションを伴って変化する
 ここでは、上記のような環境設定ウィンドウの、ウィンドウとしてのふるまいを実装していく。実際にアプリケーションの設定をする方法は別論(NSUserDefaults 等を用いる)。

 今回作成したプロジェクトは GenjiApp/PrefWindowApp - GitHub で公開するので参照のこと。

Xcode プロジェクトの作成

 Xcode を起動し、New Project... から Mac OS X Application の Cocoa Application を選択する。ここでは Project Name を PrefWindowApp とし、適当な場所にプロジェクトを作成する。

ウィンドウコントローラの追加

 プロジェクトに環境設定ウィンドウを管理する新しいファイルを追加する。New File... から Objective-C class を選択し、NSWindowController のサブクラスを作成する。名前は PreferencesWindowController とした。
 今回の環境設定ウィンドウはふたつのヴュウを切り替えて操作するものとする。
 それぞれのヴュウを判別するための tag に用いる定数を列挙型で宣言しておく。
typedef enum {
  PreferencesViewGeneral = 100,
  PreferencesViewAdvanced,
} PreferencesViewType;
 さらに、それぞれのヴュウと接続するメンバ変数として、ふたつの NSView クラスの変数を宣言する。
@interface PreferencesWindowController : NSWindowController
{
  IBOutlet NSView *generalView_;
  IBOutlet NSView *advancedView_;
}
 最後に、ヴュウを切り替えるアクションメソッドを宣言する。
- (IBAction)switchView:(id)sender;
 実装部は後回しにして、次に xib ファイルを編集する。

環境設定ウィンドウの xib ファイルを編集する

 MainMenu.xib ファイルを開く。
 Object Libaray から Object(NSObject)をドラッグしてトップレヴェルに追加する。オブジェクトの Identity Inspector で Class を先ほど作成した PreferencesWindowController に設定する。MainMenu の Preferences... メニューアイテムから PreferencesWindowController へ Control + ドラッグして showWindow: と接続する。

 Object Library から Panel(NSPanel)をキャンバスにドラッグして追加する。パネルの Attribute Inspector で以下の設定を行う。
  • Style を Regular Panel に変更
  • Controls の Resize と Minimize からチェックを外す
  • Behavior の Hide on Deactivate と Visible At Launch からチェックを外す
 次に PreferencesWindowController からパネルへ Control + ドラッグして window と接続する。

 Object Library からToolbar(NSToolbar)をパネルにドラッグして追加する。ツールバーの Attribute Inspector で Customizable からチェックを外す。
 ツールバーの Allowed Toolbar Items からすべてのツールバーアイテムを削除し、代わりに Object Library から Image Toolbar Item(NSToolbarItem)をふたつドラッグして追加する。追加したふたつのツールバーアイテムをそれぞれ Default Toolbar Items 欄にドラッグする。ツールバーアイテムの Attribute Inspector より以下のように設定する。
  • Image Name をそれぞれ NSPreferencesGeneralNSAdvanced に設定する
  • Label をそれぞれ General、Advanced に設定する
  • Palette Label をそれぞれ General、Advanced に設定する
  • Tag をそれぞれ 100101 に設定する
    この 100101PreferencesWindowController.h で宣言した列挙型の値に合わせたもの
  • Behavior の Selectable にチェックを入れる
 それぞれのツールバーアイテムからトップレヴェルオブジェクトに追加した PreferencesWindowController へ Control + ドラッグして switchView: アクションと接続する。これでツールバーアイテムをクリックしたときに switchView: アクションが呼ばれるようになる。 

 次に Object Library から Custom View(NSView)をふたつ、キャンバスにドラッグしトップレヴェルに追加する(Panel への追加ではない)。このヴュウのサイズは適当に違う大きさにしておくと、あとで実装するヴュウ切り替えによるウィンドウサイズの変更が解りやすくなる。とりあえず、解りやすいように Label(NSTextField)でも追加して適当な文字列を入力しておく。
トップレヴェルオブジェクトの PreferencesWindowController からふたつのヴュウへ Control + ドラッグして、それぞれ generalView_advancedView_ へ接続する。これで、PreferencesWindowController.m からふたつのヴュウを参照できるようになる。

環境設定ウィンドウのふるまいを実装する

 PreferencesWindowController.m を開き、以下のような switchView: メソッドを実装する。これは前述のツールバーアイテムをクリックしたときに呼ばれるアクションメソッドである。
- (IBAction)switchView:(id)sender
{
  NSWindow *window = [self window];
  NSView *contentView = [window contentView];

  NSArray *subviews = [contentView subviews];
  if([subviews count]) {
    NSView *currentView = [subviews objectAtIndex:0];
    [currentView removeFromSuperview];
  }

  NSToolbarItem *item = (NSToolbarItem *)sender;
  NSString *title = [item label];
  [window setTitle:title];

  NSView *newView = nil;
  switch([item tag]) {
    case PreferencesViewGeneral:
      newView = generalView_;
      break;
    case PreferencesViewAdvanced:
      newView = advancedView_;
      break;
    default:
      return;
  }

  NSRect windowFrame = [window frame];
  NSRect newWindowFrame = [window frameRectForContentRect:[newView frame]];
  newWindowFrame.origin.x = windowFrame.origin.x;
  newWindowFrame.origin.y = windowFrame.origin.y + windowFrame.size.height - newWindowFrame.size.height;
  [window setFrame:newWindowFrame display:YES animate:YES];

  [contentView addSubview:newView];
}
 ここで行なっていることは、
  1. ウィンドウの contentView がサブヴュウを持っている場合はそれを取り除く
  2. sender、つまりツールバーアイテムの label からウィンドウのタイトルを設定する
  3. sender、つまりツールバーアイテムの tag から切り替え先となる新しいヴュウを判別する
  4. ウィンドウと新しいヴュウの frame から、切り替え後のウィンドウの frame を計算する
  5. 計算したウィンドウの新しい frame をウィンドウにアニメーション付きで適用する
  6. ウィンドウの contentView に新しいヴュウを追加する
である。
 ウィンドウの新しい frame について、Mac OS X の座標系は左下原点なので、contentView のサイズを変更しただけでは、ウィンドウの上端が動いてしまうことになる。したがって、ヴュウ切り替え前のウィンドウの上端座標を計算し、そこから切り替え後のウィンドウの高さを引くことで、ウィンドウの新しい原点座標を得る。

 最後に、以下のような awakeFromNib メソッドを追加する。
- (void)awakeFromNib
{
  NSWindow *window = [self window];
  NSToolbar *toolbar = [window toolbar];
  NSArray *toolbarItems = [toolbar items];
  NSToolbarItem *item = [toolbarItems objectAtIndex:0];

  [toolbar setSelectedItemIdentifier:[item itemIdentifier]];
  [self switchView:item];
  [window center];
}
 awakeFromNib メソッドは xib ファイルが読み込まれ、そこに登録されたオブジェクトにアウトレット接続がなされたあとで呼ばれる。ここでツールバーアイテムの初期選択状態や、最初に表示されるヴュウの設定を行う。今回はツールバーアイテムの一番最初のものを初期状態に設定するようにしたが、選択状態を適当な方法で保存しておき、次回起動時にその状態を復元しても良いかもしれない。

ビルドして実行

 以上により、環境設定ウィンドウが完成した。アプリケーションを実行し、アプリケーションメニューから Preferences... を選択して環境設定ウィンドウを表示してみる。ツールバーアイテムのクリックでヴュウが切り替わり、同時にウィンドウサイズの変更がアニメーションを伴って変更されることを確認する。

 今回作成したプロジェクトは GenjiApp/PrefWindowApp - GitHub で公開するので参照のこと。

2011年12月3日土曜日

Mac App Store に提出するアプリケーションに Spotlight/Quick Look プラグインを同梱する方法

アプリケーションの Xcode プロジェクトと、Spotlight/Quick Look プラグインのプロジェクトがあるとする。
  1. アプリケーションのプロジェクトを開いて、Project navigator にプラグインのプロジェクトをドラッグ・アンド・ドロップする
  2. プラグインのプロジェクトの Build Settings にて Skip Install を Yes に設定する
  3. アプリケーションのターゲットにて、Build Phases タブで Add Build Phase で Add Copy Files を選択して追加する。Copy Files の Destination には Wrapper、Subpath はそれぞれ Contents/Library/SpotlightContents/Library/QuickLook を指定する。コピィするファイルはそれぞれのターゲットを指定する
  4. Build Phases にて Add Build Phases で Add Run Script を選択して追加し、以下のようなコマンドを入力する
    codesign -v -s "${CODE_SIGN_IDENTITY}" ${CODESIGNING_FOLDER_PATH}/Contents/Library/Spotlight/EPUBImporter.mdimporter
    codesign -v -s "${CODE_SIGN_IDENTITY}" ${CODESIGNING_FOLDER_PATH}/Contents/Library/QuickLook/EPUBQLGenerator.qlgenerator
    署名は提出用だけでいいので、Run script only when installing にチェックを入れておく
  5. Archive して完成
 手順 2 について、Skip Install を Yes にしておかないと、手順 5 でアプリケーションがうまくアーカイヴできない。
 手順 3 でアプリケーションパッケージ内に各プラグインを配置。
 手順 4 でプラグインに署名を行う。以前まではプラグインに署名せずとも大丈夫だったが、ちょっと前からプラグインにも署名をしないといけなくなった。