最小限データーベースその3

コピペとDrag&Dropのデータタイプを増やす。

今までは、自分自身が作成した、データタイプ “com.mindto01s.M3XNote” しかコピペもDrag&Dropも出来なかった。

そのため、newコマンドで項目ノートを作成してから、TextViewへデータをペーストしていた。

これを、Pasteboard に、以下のデータタイプがある場合は、ペーストコマンドで自動で項目ノートの作成とそのノートへのペーストを一手間でできるようにした。

Dropの受け入れ

[M3XViewController viewDidLoad] で [M3XNote pasteBoardRowsType]を[M3XNote pasteBoardRowsTypes]とするだけ。

[self.browserView registerForDraggedTypes:[M3XNote pasteBoardRowsTypes]];

[M3XNote pasteBoardRowsTypes]の方では、以下のように受け入れ可能なUTIを列挙している。

+ (NSArray<NSString*>*) pasteBoardRowsTypes
{
    return @[
             [self pasteBoardRowsType],
             NSPasteboardTypeRTFD,
             NSPasteboardTypeRTF,
             NSPasteboardTypeString,
             NSPasteboardTypePNG, // <-- 入力のみ
             NSPasteboardTypeTIFF,  // <-- 入力のみ
             ];
}

この変更だけで、Dropはは可能になる。もちろんDropした後のコードは何も書いていないので、Dropした瞬間に落ちるはず。

ペーストボードへの書き込み

この変更は、[M3XNote wreiteToPasteboard:]の中だけ。

ペーストボードには複数のデータタイプを入れられるので、今までのコードの末尾に、書き出したいデータのコードを書き足すだけ。

- (void) wreiteToPasteboard:(NSPasteboard*)pasteboard
{
    .
    .
    .

    // 以下から書き足したコード
    NSAttributedString* theAttributedString = [[NSAttributedString alloc] initWithRTFD:self.memo documentAttributes:nil];

    [pasteboard setData:[theAttributedString RTFDFromRange:NSMakeRange(0, theAttributedString.length)
                                        documentAttributes:@{}]
                forType:NSPasteboardTypeRTFD];

    [pasteboard setData:[theAttributedString RTFFromRange:NSMakeRange(0, theAttributedString.length)
                                       documentAttributes:@{}]
                forType:NSPasteboardTypeRTF];

    [pasteboard setString:theAttributedString.string
                  forType:NSPasteboardTypeString];
}

ペーストボードに対してsetData:forTypeなどで書き出している。

ここまでのコードを書くと、このアプリの項目ノートを他のテキストエディターなどペースト出来るようになる。

ペーストボードからの読み出し

読み出しのコードはかなり汚い。

上から順番に、欲しいデータが存在したら処理をしている。独自データが最優先で、次にRTFDなどの文字列系が続き、最後は画像データの順番になっている。

RTFDなどの文字列系は以下のように単純に、NSData型に変換してCoreDataに入れているだけ。

+ (instancetype) readFromPasteboard:(NSPasteboard*)pasteboard
             inManagedObjectContext:(NSManagedObjectContext *)context
{
    M3XNote* theResult = nil;

    .
    .
    .
    // RTFD
    NSData* theRTFD = [pasteboard dataForType:NSPasteboardTypeRTFD];

    if( theRTFD != nil )
    {
        theResult = [NSEntityDescription insertNewObjectForEntityForName:@"M3XNote"
                                                  inManagedObjectContext:context];

        theResult.memo = theRTFD;
        theResult.isHome = NO;

        return theResult;
    }
    .
    .
    .
}

最後の画像系のデータだけ少しだけ複雑なことを行なっている。

NSTextViewはRTFDのデータを表示する機能はあるが、NSImageViewのように直接Imageを設定はできない。 そのため、NSTextAttachmentを使用して、画像を貼り付ける形になる。

+ (instancetype) readFromPasteboard:(NSPasteboard*)pasteboard
             inManagedObjectContext:(NSManagedObjectContext *)context
{
    M3XNote* theResult = nil;

    .
    .
    .
    // PNG or TIFF
    NSData* theImageData;
    theImageData = [pasteboard dataForType:NSPasteboardTypePNG];
    if( theImageData == nil )
    {
        theImageData = [pasteboard dataForType:NSPasteboardTypeTIFF];
    }

    if( theImageData != nil )
    {
        theResult = [NSEntityDescription insertNewObjectForEntityForName:@"M3XNote"
                                                  inManagedObjectContext:context];

        NSDateFormatter* theDateFormatter = [[NSDateFormatter alloc] init];
        [theDateFormatter setDateFormat:@"yy/MM/dd H:mm:ss"];

        NSString* theMemoString = [NSString stringWithFormat:@"画像 : %@\n", [theDateFormatter stringFromDate:[NSDate date]]];

        NSMutableAttributedString* theAttributedString = [[NSMutableAttributedString alloc] initWithString:theMemoString];

        NSTextAttachment* theAttachment = [[NSTextAttachment alloc] init];
        theAttachment.image = [[NSImage alloc] initWithData:theImageData];
        NSAttributedString* theAttatchText = [NSAttributedString attributedStringWithAttachment:theAttachment];

        [theAttributedString appendAttributedString:theAttatchText];

        theResult.memo = [theAttributedString RTFDFromRange:NSMakeRange(0, theAttributedString.length)
                                         documentAttributes:@{}];
        theResult.isHome = NO;

        return theResult;
    }
    .
    .
    .
}

ここまで出来れば、他のアプリからこのアプリへコピー&ペーストが出来るようになる。

今後の課題

実装していないものとして、URLとhtmlのデータタイプがある。これがないために、SafariからURLのDrag&Dropで項目ノートを作成することは出来ない。

これは、現在のアプリのデータ構造の持ち方では、サーバーからデータを取得する時に、メインスレッドをブロックする必要がありそうなので、実装しなかった。

同じく、ファイルDropも実装しなかった。画像ファイルやRTFDファイルをDrop可能にしようかとも考えたが、サイズの大きいデータを考慮していない設計なのでこれも実装しなかった。

今回はここまで。多分続きはなし。( microMemex3.zip )

なんか、code-blockが上手く働いてくれないなぁ。でも、眠いから寝る。

最小限データーベースその2

前回に課題として残っていた機能を実装する。

  1. コピー&ペーストを実装し忘れていた問題
  2. 最上位のカラムへDropできない問題
  3. Drag&Drop時のドリルダウンアニメーションに、TextViewが追随しない問題

上記の3件の実装は前回のコードに少しだけ追加の記述をすることで実装出来た。 変更があるのは、M3XViewControllerクラスだけ。

なお、以下の2件は、NSBrowserのサブクラス化が必要になりそうなので、今回はパス。

  1. TrashCanへのDropで削除できない問題
  2. NSBrowser の focus ring が適切に表示されない問題

コピー&ペーストの実装

コピペのコードは、Drag&Dropのコードの流用をしています。

そもそも、Drag&Dropは派手なコピー&ペーストなのでコード自体は同じようなものになります。

コピペメニューの実行

cutを実行するコードは以下の通り。

コピーして削除している。deleteは実装済みなので、copyさえ出来ればこのコードで動く。

- (IBAction) cut:(id)sender
{
    [self copy:sender];
    [self delete:sender];
}

copyを実行するコードは以下の通り。

選択箇所を取得して、pasteboardに書き込むだけ。Drag&DropのコードのDrag開始のコードとほぼ同じ。

異なるのは、書き込むpasteboardがドラッグ用からクリップボード用のgeneralPasteboardに変わっただけ。

- (IBAction) copy:(id)sender
{
    NSIndexPath* theSelectedPath = self.browserView.selectionIndexPath;

    // 選択されているitemの取得
    M3XNote* theSelectedNote       = [[self rootItemForBrowser:self.browserView]
                                      noteFromIndexPath:theSelectedPath];

    // ペーストボードへ書き込み
    [theSelectedNote wreiteToPasteboard:[NSPasteboard generalPasteboard]];
}

pasteを実行するコードも簡単。Dropするコードとほぼ同じ。

pasteboardの変更もcopyメソッドの変更と同じ。

 - (IBAction) paste:(id)sender
 {
     NSIndexPath* theSelectedPath = self.browserView.selectionIndexPath;

     // 選択されているitemの取得
     M3XNote* theSelectedNote = [[self rootItemForBrowser:self.browserView]
                                 noteFromIndexPath:theSelectedPath];

    // ここでCoreDataに追加する
    M3XNote* theNoteFormPasteboard = [M3XNote readFromPasteboard:[NSPasteboard generalPasteboard]
                                          inManagedObjectContext:self.managedObjectContext];

    [theSelectedNote addLinksObject:theNoteFormPasteboard];
}

前回の資料を参考にすれば特に難しいところはないはず。

メニューのenable/disableの制御機構

validateUserInterfaceItemについては過去に何度も書いているので検索してください。

コードを簡潔に表記できるように、「メソッド名の合成」を積極的に使用したコードにしています。

このvalidateUserInterfaceItem部分は、わかりにくいので、2013年頃の記事を参考に色々と悩んでいただけると嬉しい。

cutを許可するコード。単純に選択されていればcutできるようにしています。

- (BOOL) canCut:(nullable id<NSValidatedUserInterfaceItem>)item
{
    return self.browserView.selectionIndexPath != nil;;
}

copyを許可するコード。これも単純に選択されていればcutできるようにしています。

- (BOOL) canCopy:(nullable id<NSValidatedUserInterfaceItem>)item
{
    return self.browserView.selectionIndexPath != nil;;
}

pasteを許可するコード。クリップボードの中のデータをチェックしています。記憶が曖昧ですが、NSDataに変更する前に、どの型が入っているかをチェックするメソッドがあった気がします。ですので以下のコードは動きますが、よくないコードのはずです。

- (BOOL) canPaste:(nullable id<NSValidatedUserInterfaceItem>)item
{
    NSIndexPath* theSelectedPath = self.browserView.selectionIndexPath;

    // 選択されているitemの取得
    M3XNote* theSelectedNote       = [[self rootItemForBrowser:self.browserView]
                                      noteFromIndexPath:theSelectedPath];

    // もう少しスマートな書き方があるはず
    NSPasteboard*   thePasteboard = [NSPasteboard generalPasteboard];
    NSData*         theData = [thePasteboard dataForType:[M3XNote pasteBoardRowsType]];

    return (theSelectedNote != nil) && (theData != nil);
}

コピペの実装はこんな感じ。勉強会で説明したように、コピペのコードは非常に簡単です。

最上位のカラムへDropできない問題への対処

2つのメソッドの修正になる。

Dropを許可するメソッド、browser:validateDrop:proposedRow:column:dropOperation: と、

Drop後にデータを変更するメソッド browser:acceptDrop:atRow:column:dropOperation: の2つ。

必要な情報は、以下の3つ。

  1. 最上位のカラムへDropとは、項目(row)がない部分のcolumへのDrop。NSBrowserDropOperationがNSBrowserDropAboveのものを扱うことになる。
  2. NSBrowserDropOnは項目の上、NSBrowserDropAboveは項目と項目の間を意味している。
  3. rowが-1の時にcolumn全体を意味する。

すると、以下のようなコードになる。

NSBrowserDropAboveであれば、Dropはカラム全体へのDropと見なすようにする。

// Drop先
- (NSDragOperation)browser:(NSBrowser *)browser
              validateDrop:(id <NSDraggingInfo>)info
               proposedRow:(NSInteger *)row
                    column:(NSInteger *)column
             dropOperation:(NSBrowserDropOperation *)dropOperation
{
    // item間やitem以外の場所へのDropはカラム全体へのDropと見なす
    if( *dropOperation == NSBrowserDropAbove ) //<---追加した部分の開始
    {
        *row = -1;
        *dropOperation = NSBrowserDropOn;
    }                                          //<---追加した部分の終了

    if( *dropOperation == NSBrowserDropOn )
    {
        // NSDraggingInfoから
        if( info.draggingSource == self.browserView )
        {
            return NSDragOperationLink;
        }
        else
        {
            return NSDragOperationCopy;
        }
    }

    return NSDragOperationNone;
}

Drop後の処理は先ほどのDrop前の処理がわかれば簡単で、row == -1でリンクを貼るノードを変えるだけ。

// Drop後の処理
- (BOOL)  browser:(NSBrowser *)browser
       acceptDrop:(id <NSDraggingInfo>)info
            atRow:(NSInteger)row
           column:(NSInteger)column
    dropOperation:(NSBrowserDropOperation)dropOperation
{
    // まず、Drop先のNoteを求める。
    // pasteboardにデータをNoteのObjectID入れること
    M3XNote* theParentNote =  [self.browserView itemAtIndexPath:
                               [self.browserView indexPathForColumn:column]];

    M3XNote* theDropNote;

    if( row == -1 )// カラム全体
    {
        theDropNote = theParentNote;
    }
    else
    {
        // Dragできるのは一つだけ
        theDropNote = theParentNote.sortedLinks[row];
    }

    // ここでCoreDataに追加する
    M3XNote* theDraggedNote = [M3XNote readFromPasteboard:info.draggingPasteboard
                                   inManagedObjectContext:self.managedObjectContext];

    [theDropNote addLinksObject:theDraggedNote];

    return YES;
}

Drag&Drop時のドリルダウンアニメーションに、TextViewが追随しない問題

これは正しい解決方法かは不明だが、動く。

// Drag&DrilDown時にDrop先のTextViewへ内容へ変更する小細工
- (void)browser:(NSBrowser *)browser didChangeLastColumn:(NSInteger)oldLastColumn toColumn:(NSInteger)column
{
    self.selectedNode = [[self rootItemForBrowser:
                          self.browserView] noteFromIndexPath:self.browserView.selectionIndexPath];
}

lastColumnが変更されれる時に、self.selectedNodeを更新している。

self.selectedNodeの更新に合わせて、TextViewの内容が変更されるので動く。

また、Drag処理が終わると、NSBrowserのselectionIndexPathが復帰するので、そこそこうまく動いているように見える。

今回はここまで。( microMemex2.zip )

「ヴォイニッチの書棚」のバックナンバーの取得

もう随分前に番組は終了したのですが、「ヴォイニッチの書棚」というpodcastが好きでした。長時間の移動時によく聞いてました。

また聴きたくなったので、iTunesの中を探したが最後の20回分しか出てこない。

クリラジのサーバー には音源があるようなので、ちょいとxmlを吐く使い捨てコードを書いてみた。

使い方。

ソースを実行するとクリップボードに、xmlを書き込む動作をする。

  1. 以下のように、適当なファイルにペーストする。
pbpaste > voi-book.xml
  1. 出来上がったファイルを適当なサーバーにアップする。
適当にアップロードする。
  1. itpcスキームでURLを叩いてitunesに取り込む

以下のようにitunesに投げる。

open itpc://アップしたドメイン名/適当なパス/voi-book.xml
  1. 以下の場所に生成したvoi-book.xmlをアップしたので使えるかも。

voi-book.xml

ソースコードは、 backNumber-voi.zip

以下の呼び出しで、itunesが起動するかもしれません。自分の分はダウンロードが終わったので試してません。

open itpc://www.mindto01s.com/_downloads/voi-book.xml

作ってみての感想

Objective-Cで書いたのだが、スクリプト言語で書くべきだったな。

スクリプト言語は知らないけど。