Cocoa アプリで Cover Flow を使う方法を試してみた

すごくいまさらな気がするけれど, d:id:hetima:20080314:1205423499 を参考に,Cover Flowを表示させてみた。
さすがOpenGLでフレームレート高いなぁ。

IKImageFlowViewを表示するだけ

テーブルビューに表示させるような感じで,項目のNSArrayControllerがあるなら,
IKImageFlowViewのdelegateでは,NSArrayControllerのarrangedObjectとselectionをobserveして,
Cover Flow用のアイテム配列を作りなおしたり,IKImageFlowViewに-setSelectedIndex:したりすればいい。

IKImageFlowViewに渡すアイテムの実装

Cover Flow用のアイテムは,imageUID, imageRepresentationType, imageRepresentationを返せればいい。
イメージのロードは,imageRepresentationが呼ばれるまで遅延しておくこともできる。
Cover Flowで選択中の項目の下に表示される文字列は,imageTitleとimageSubtitleを返すことで設定できる。
ほとんど@propertyで書けばいいだけだから,あまりコーディングしなくていい。

IKImageFlowViewのリサイズ

IKImageFlowViewの-setShowSplitter:にYESを渡すと出てくるサイズ変更用のディバイダをクリックすると,
delegateの-imageFlow:startResizingWithEvent:が呼ばれる。
他にこのディバイダでのデリゲートメソッドは無いみたいだから,このメソッド内でリサイズを処理するということだろう。


iTunesみたいに,Cover Flowが一番上にある状態のNSSplitViewを使っているなら,次のようにする。
ただしflowViewとsplitViewは,それぞれIKImageFlowViewと,それを含むNSSplitView。

- (void)imageFlow:(id)fview startResizingWithEvent:(NSEvent *)theEvent
{
    NSPoint offset = [flowView convertPoint:[theEvent locationInWindow] fromView:nil];
    
    while (theEvent = [[flowView window] nextEventMatchingMask:(NSLeftMouseDraggedMask|NSLeftMouseUpMask)]){
        if (NSEventMaskFromType([theEvent type]) == NSLeftMouseUpMask) break;
        
        NSPoint p = [splitView convertPoint:[theEvent locationInWindow] fromView:nil];
        [splitView setPosition:p.y+offset.y ofDividerAtIndex:0];
    }
}

-nextEventMatchingMask:ってあまり使わないけれど,Appleのサンプルコードではたまに見かけるような気がする。
メソッドを終了させずに,その場でイベントループを回せるような感じ。

リサイズ周りの細かい調整

IKImageFlowViewの高さを小さくしすぎると,一度に表示する項目が多くて重くなるのと,座標軸が潰れて(?)おかしなことになる。
サブビューの大きさを制限できるNSSplitViewを使うか作るか,NSSplitViewのデリゲートで制限してやるのがよさそう。
高さの制限は128.0くらいがいいかな。

  • -splitView:constrainMinCoordinate:ofSubviewAt:では,128.0を返す。ディバイダで128.0より小くできなくなる。
  • -splitView:effectiveRect:forDrawnRect:ofDividerAtIndex:では,NSZeroRectを返す。もともとのNSSplitViewのディバイダを操作できなくなる。


これでも,windowのリサイズでの自動リサイズでは高さが小さくなりすぎることがあるので,

  • splitView:resizeSubviewsWithOldSize:で回避する。
- (void)splitView:(NSSplitView *)theSplitView resizeSubviewsWithOldSize:(NSSize)oldSize
{
    NSRect flowViewFrame = [flowView frame];
    [splitView adjustSubviews]; // デフォルトのリサイズ処理
    
    // splitView自体が128.0より潰れてしまうのを防ぐ
    if ([flowView frame].size.height < 128.0){
        [splitView setFrameSize:NSMakeSize([splitView frame].size.width, oldSize.height)];
    }
    
    // Cover Flowの高さを変更しない
    [splitView setPosition:flowViewFrame.size.height ofDividerAtIndex:0];
    return;
}

そしてcom.apple.safari.bookmarkから継承したときの問題

Finderの検索結果で出てきたファイルは開けるけど,Spotlightメニューの検索結果で出てきたファイルを選択すると,
*** -[NSURL initWithString:relativeToURL:]: nil string parameter
をSpotlightが出力するだけで,開けない。
なんか余計なことしてるなぁ。どうすればいいのか。

自前のUTIを定義(とりあえずpublic.dataから継承)して,そのためのMDImporterを作ってメタデータを作成。

これで自前のデータ形式をSpotlightで検索できる。


ファイル名と,検索結果の表示を変えるには,kMDItemDisplayNameを設定すればいい。
kMDItemDisplayNameの内容が検索結果に表示される。

しかし,このやり方では問題がある。

検索結果を表示し,結果の名前部分をシングルクリックまたはReturnキーを押すと,普段のFinderと同じようにファイル名編集モードに入る。
そのときのファイル名はkMDItemDisplayNameのものになっているから,Return→Returnとするだけで,本来のファイル名がkMDItemDisplayNameの内容に書き変わってしまう。
また,ファイル名編集モードに入った覚えがなくても勝手にファイル名が変わってしまうということもあった。


kMDItemDisplayNameは日本語やファイル名にふさわしくない文字を含んでいるかもしれないし,他のファイルと同じ内容が入っているかもしれないので,この挙動は危険である。
そもそも,アプリ側またはユーザ側の都合によって,ファイル名を変えたくないこともある。

問題が発生しない場合もある。

例えば,Safariのブックマークの検索結果。
kMDItemDisplayNameによって,ファイル名と検索結果の表示が変わっているが,検索結果の上でファイル名を変更することができない。また,パスバーにファイルパスが表示されない。

この違いがどこから発生するかは謎だけれど,

どうやらFinder.appやSpotlight.app内部のMDOperationsAllowed.plistとか,CoreServices.frameworkのMetadata.framework内部のMDPredicate.plistに直接書かれている様子。
これ以外に関連するファイルをまだ見つけられていないので,ハックにならない通常のカスタマイズができるのか分からない。

正しいやり方は分からないけれど,とりあえずファイル名が勝手に変わるのを防ぐには,

自前のUTIをcom.apple.safari.bookmarkから継承するように変更すればいい。
これで検索結果でファイル名の編集が出来無くなるし,ファイルパスの表示も無くなる。
もちろんSpotlightの検索結果では,Webpagesのカテゴリに表示されることになる。


このUTIはどうみてもブックマークじゃないなと思ったら,他の,検索結果でファイルパスが表示されないUTIから継承するようにすればいい。MDOperationsAllowed.plistが参考になるかもしれない。
いい継承元UTIが見つからなければ,諦めるしか。

QTMovieのcurrentFrameImageを改善する

QTKitのQTMovieの

- (NSImage *)currentFrameImage;
- (NSImage *)frameImageAtTime:(QTTime)time;

あたりは,使うたびにメモリリークする。数回呼ぶくらいならどうってことないけど,大量に呼ぶと大変なことに。


この問題を回避するには,QuickTimeAPIを使わなければいけない??。
MovieとかGWorldとかよく分からないんだけど…。


とりあえず,いろいろ試行錯誤してできたのが,フレーム画像を大量に呼び出しても大丈夫なクラス。
でもなぜこれでいいのかは,謎(SetMovieGWorldすると,それが開放されないような?)。

@interface QTFastMovie : QTMovie
{
    GWorldPtr gworld;
}
- (NSImage *)fastCurrentFrameImage;
@end

@implementation QTFastMovie
- (GWorldPtr)offScreenGWorld
{
    if(!gworld){
        Rect srcRect;
        GetMovieBox([self quickTimeMovie],&srcRect);
        
        NewGWorld(&gworld,
                    k32ARGBPixelFormat,
                    &srcRect,
                    NULL,
                    NULL,
                    0);
        
        SetMovieGWorld([self quickTimeMovie], gworld,NULL);
    }
    return gworld;
}
- (NSImage *)fastCurrentFrameImage
{
    Movie movie = [self quickTimeMovie];    
    GWorldPtr offscreen = [self offScreenGWorld];
    
    MoviesTask(movie, 0);
    NSImage *image = imageFromGworld(offscreen);
    
    return image;
}
@end


imageFromGWorld()は,
HMDT > Cocoa Programming Tips 1001 > GWorldをNSImageに変換する
のもの。


でもTigerで使うと,いくつもDeprecatedって警告がでる。
Leopardでは,そもそもQTKitのメモリリークの問題がなおるのか,TigerからはCoreVideoを使えということなのか…?。

Core Data Document-based Applicationでファイルパッケージを扱う (2)


このエントリの実装で,とりあえず画像の読み込みまでできるようになります。てかそれなんてただのCoreData。
ファイルパッケージじゃなくて普通のファイルとしてなら,CoreDataの保存と読み込みもできるようになるはず。

  • AppDelegateの -application:openFiles:
  • MyDocumentの -addFile:
  • PhotoMOの作成と実装

ただし,-application:openFiles:を経由するためには,メニューからOpenではなく,ドックのアプリアイコンへドロップする必要あり。
メニューからに対応するにはどっかのっとらなきゃだめ?。

続きを読む