最小限データーベースその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 )