順番を書き換える操作の説明【実践的Macintoshプログラミング解説】

印刷用表示 |テキストサイズ 小 |中 |大 |

CoreData版 Repeating Motif Generator の開発 Repeating Motif Wonderland CoreData 実践的 Macintosh プログラミング解説

LinkIconホーム

更新日 2009-05-24

ドラッグ&ドロップでリサージュ図形の並べ替え、複製ができる様にする

順番を書き換える操作の説明

 前のページでtableView:acceptDrop:row:dropOperation:メソッドのソースコードを示しましたが、ここで何をやっているのかは図で説明した方がよいと思いますので、改めてここで説明します。

オブジェクトがどのように配置されているのか

DnD01.png
 テーブルビューの中に並んでいるオブジェクトと、配列コントローラのarrangedObjectsというNSArrayは一対一で対応しています。

 右の図で番号付きの四角形はarrangedObjectsの中に入っているオブジェクトを表すものとします。番号はオブジェクトのorderです。変更前はorderでソートしてあるので、この数字は配列のindexと一致しています。これは同時にテーブルビューの並び順でもあります。

 さてテーブルビューでオレンジ色のオブジェクトをドラッグし、矢印の位置にドロップしたとします。このとき、tableView:acceptDrop:row:dropOperation:メソッドの引数rowは2となります。各ドロップ場所とrowの関係は図の左端に示した通りとなります。

 tableView:writeRowsWithIndexes:toPasteboard:メソッドの第二引数で渡されるNSIndexSetの中には3と4が入っています。3行目と4行目という意味です。(数え方はゼロベース)

 ユーザがこのような操作を行なった場合、orderをどのように書き換えればよいのかが課題となります。結論は図の右端に示した通りですが、このような書き換えを実現するアルゴリズムについて考えていきましょう。

 この様に書き換えた後で配列コントローラにrearrangeObjectsメッセージを送れば、orderでソートされてarrangedObjectsとテーブルビューの並び順が適切に書き変わります。(事前にソートディスクリプタを設定しておく必要があります)

書き換えが発生する範囲について考える

DnD02.png
 まずarrangedObjectsのどこからどこまでを書き換えればよいのかを考えます。

 上方向へ移動する場合でも、下方向へ移動する場合でも、orderの書き換えが発生するのはドラッグされたオブジェクトと挿入先の行に挟まれた範囲に限定されます。
 それより上も、それより下もorderの書き換えは発生しません。

 したがって書き換え範囲の最初をfirst、最後をlastとして以下のコードの様に決めると、右図の場合下にドラッグする左側はfirst=1, last=5、上にドラッグする右側はfirst=2, last=4となります。

 for(i=first;i<last;i++)とした場合、下にドラッグする場合はちょうど良いのですが、上にドラッグする場合一行少なくなってしまいます。それでも問題ない様にしておく必要があります。

//
//  DragDropDataSource.m
//

- (BOOL)tableView:(NSTableView*)tableView 
       acceptDrop:(id <NSDraggingInfo>)info 
              row:(int)row 
    dropOperation:(NSTableViewDropOperation)operation
{
    NSPasteboard    *pboard = [info draggingPasteboard];
    int             first,last;
    NSIndexSet      *rowIndexes;

    rowIndexes = [NSUnarchiver unarchiveObjectWithData:[pboard dataForType:RMGRowsType]];

    first = row < [rowIndexes firstIndex] ? row : [rowIndexes firstIndex];
    last = row > [rowIndexes lastIndex] ? row : [rowIndexes lastIndex];
}

下へドラッグする場合

DnD03.png
 オブジェクトを下へドラッグする場合について、右の図を例として考えてみましょう。

 この場合、first=1, last=5, row=5となります。

 まずfirstからrowまでの範囲でドラッグされていないオブジェクトのorderを振り直します。振り直した結果が中央の列です。(first-1)番目のオブジェクトのorderが0ですからその続きの番号から振っていきます。

 次にドラッグされたオブジェクトのorderを振り直します。振り直した結果が右の列です。ドラッグされていないオブジェクトの最後のorderが2ですからその続きの番号から振っていきます。

 下へドラッグする場合はこれで完了です。この手順を実装すると、以下のコードとなります。

 firstがゼロの場合もあり得るので、その場合(first-1)番目のオブジェクトはありません。そのときはorderをゼロからスタートさせればOKです。

//
//  DragDropDataSource.m
//

- (BOOL)tableView:(NSTableView*)tableView 
       acceptDrop:(id <NSDraggingInfo>)info 
              row:(int)row 
    dropOperation:(NSTableViewDropOperation)operation
{
    NSPasteboard    *pboard = [info draggingPasteboard];
    int             i,order,index,first,last;
    NSIndexSet      *rowIndexes;
    NSManagedObject *object;
    NSArray         *arrangedObjects = [arrayController arrangedObjects];

    rowIndexes = [NSUnarchiver unarchiveObjectWithData:[pboard dataForType:RMGRowsType]];

    //  挿入された行までのドラッグされていないオブジェクトの番号を振り直す
    first = row < [rowIndexes firstIndex] ? row : [rowIndexes firstIndex];
    if(first == 0)
        order = 0;
    else
        order = [[[arrangedObjects objectAtIndex:first-1] order] intValue] + 1;
    for(i=first;i<row;i++)
    {
        if([rowIndexes containsIndex:i])    continue;
        object = [arrangedObjects objectAtIndex:i];
        [object setValue:[NSNumber numberWithInt:order++] forKey:@"order"];
    }
    //  ドラッグされているオブジェクトの番号を振り直す
    index = [rowIndexes firstIndex];
    while(index != NSNotFound)
    {
        object = [arrangedObjects objectAtIndex:index];
        [object setValue:[NSNumber numberWithInt:order++] forKey:@"order"];
        index = [rowIndexes indexGreaterThanIndex:index];
    }
}

上へドラッグする場合

DnD04.png
 オブジェクトを上へドラッグする場合について、右の図を例として考えてみましょう。

 下へドラッグする場合と同様に、まずfirstからrowまでの範囲でドラッグされていないオブジェクトのorderを振り直す事を考えます。ところがfirst=1, row=1ですからfirstからrowまでの範囲というものがありません。逆に言えば同じ処理をしてもここは自動的に飛ばされるという事です。

 次にドラッグされたオブジェクトのorderを振り直します。振り直した結果が中央の列です。続きの番号から振っていけばOKです。

 上へドラッグする場合は続きがあります。rowからlastまでの範囲でドラッグされていないオブジェクトのorderを振り直します。下へドラッグする場合は、逆にrowからlastまでの範囲というものがないので、ここが自動的に飛ばされる事になります。

 さて、ここでlastまでの範囲が本来よりも一行少ないという事を思い出して下さい。これは問題となるでしょうか?

 結論から言うと問題とはなりません。一行少ないのですが、一番下は必ずドラッグされたオブジェクトなので、orderを振り直す対象外なのです。上へドラッグするのだから、一番下はドラッグされるオブジェクトなのです。

 この手順を実装すると、以下のコードとなります。 

//
//  DragDropDataSource.m
//

- (BOOL)tableView:(NSTableView*)tableView 
       acceptDrop:(id <NSDraggingInfo>)info 
              row:(int)row 
    dropOperation:(NSTableViewDropOperation)operation
{
    NSPasteboard    *pboard = [info draggingPasteboard];
    int             i,order,index,first,last;
    NSIndexSet      *rowIndexes;
    NSManagedObject *object;
    NSArray         *arrangedObjects = [arrayController arrangedObjects];

    rowIndexes = [NSUnarchiver unarchiveObjectWithData:[pboard dataForType:RMGRowsType]];

    //  挿入された行までのドラッグされていないオブジェクトの番号を振り直す
    first = row < [rowIndexes firstIndex] ? row : [rowIndexes firstIndex];
    if(first == 0)
        order = 0;
    else
        order = [[[arrangedObjects objectAtIndex:first-1] order] intValue] + 1;
    for(i=first;i<row;i++)
    {
        if([rowIndexes containsIndex:i])    continue;
        object = [arrangedObjects objectAtIndex:i];
        [object setValue:[NSNumber numberWithInt:order++] forKey:@"order"];
    }
    //  ドラッグされているオブジェクトの番号を振り直す
    index = [rowIndexes firstIndex];
    while(index != NSNotFound)
    {
        object = [arrangedObjects objectAtIndex:index];
        [object setValue:[NSNumber numberWithInt:order++] forKey:@"order"];
        index = [rowIndexes indexGreaterThanIndex:index];
    }

    //  挿入された行以降のドラッグされていないオブジェクトの番号を振り直す
    last = row > [rowIndexes lastIndex] ? row : [rowIndexes lastIndex];
    for(i=row;i<last;i++)
    {
        if([rowIndexes containsIndex:i])    continue;
        object = [arrangedObjects objectAtIndex:i];
        [object setValue:[NSNumber numberWithInt:order++] forKey:@"order"];
    }
}

欠番に対応する

 さて、説明の便宜上これまで触れてきませんでしたが、実はドラッグ開始時にorderがゼロから順番にきれいに並んでいるとは限らないのです。次はこの問題に対処します。

 なぜゼロから順番にきれいに並んでいるとは限らないのかというと、オブジェクトがカットされるとそこが欠番になるからなのです。欠番にならない様にする事は可能ですが、敢えて欠番にしています。こうしておくとアンドゥした時に元の位置に戻るからです。

 したがって欠番のある状態で、それぞれのオブジェクトのorderを変えずに並べ替える必要があります。これは実は簡単な話で、最初に対応表を作っておき今まで数値を指定していたところを、その対応表で読み替えればよいだけの話です。

//
//  DragDropDataSource.m
//

- (BOOL)tableView:(NSTableView*)tableView 
       acceptDrop:(id <NSDraggingInfo>)info 
              row:(int)row 
    dropOperation:(NSTableViewDropOperation)operation
{
    NSPasteboard    *pboard = [info draggingPasteboard];
    int             i,order,index,first,last;
    NSIndexSet      *rowIndexes;
    NSManagedObject *object;
    NSArray         *arrangedObjects = [arrayController arrangedObjects];
    NSEnumerator    *enumerator = [arrangedObjects objectEnumerator];
    NSMutableArray  *orders = [NSMutableArray arrayWithCapacity:[arrangedObjects count]];

    while(object = [enumerator nextObject])
        [orders addObject:[object order]];

    rowIndexes = [NSUnarchiver unarchiveObjectWithData:[pboard dataForType:RMGRowsType]];

    //  挿入された行までのドラッグされていないオブジェクトの番号を振り直す
    first = row < [rowIndexes firstIndex] ? row : [rowIndexes firstIndex];
    if(first == 0)
        order = 0;
    else
        order = [[[arrangedObjects objectAtIndex:first-1] order] intValue] + 1;
    for(i=first;i<row;i++)
    {
        if([rowIndexes containsIndex:i])    continue;
        object = [arrangedObjects objectAtIndex:i];
        [object setValue:[orders objectAtIndex:order++] forKey:@"order"];
    }
    //  ドラッグされているオブジェクトの番号を振り直す
    index = [rowIndexes firstIndex];
    while(index != NSNotFound)
    {
        object = [arrangedObjects objectAtIndex:index];
        [object setValue:[orders objectAtIndex:order++] forKey:@"order"];
        index = [rowIndexes indexGreaterThanIndex:index];
    }
    //  挿入された行以降のドラッグされていないオブジェクトの番号を振り直す
    last = row > [rowIndexes lastIndex] ? row : [rowIndexes lastIndex];
    for(i=row;i<last;i++)
    {
        if([rowIndexes containsIndex:i])    continue;
        object = [arrangedObjects objectAtIndex:i];
        [object setValue:[orders objectAtIndex:order++] forKey:@"order"];
    }
}

最終的なコード

 肝心な部分の説明は終わりました。あと少しコードを付け足せば、最終的なコードになります。

 ここまでドラッグ&ドロップでオブジェクトが移動する場合の事しか考えていませんでしたが、オブジェクトをドラッグコピーする場合もあります。どちらなのかをtableView:validateDrop:で判断して、ドラッグコピーの場合はペーストの処理を実行します。

 ドラッグコピーの場合はメニューからペーストコマンドを実行した時とは違った処理が必要なのですが、それはmanagedObjectChanged:メソッドに任せます。違いを見分けるためにinsertionRowを設定しておきます。managedObjectChanged:メソッドの説明はつぎのページにまわします。

 orderを振り直したら配列コントローラにrearrangeObjectsメッセージを送れば、リサージュ図形が並べ直されて処理完了です。ただしここで単に配列コントローラにrearrangeObjectsメッセージを送ると、アンドゥした時に不具合が発生します。アンドゥしても見かけ上元に戻らないのです。

 orderのアンドゥはCoreDataがサポートしているので、アンドゥするとorderの値は元に戻るのですが、配列コントローラにrearrangeObjectsメッセージが送るところまでCoreDataはサポートしてくれません。rearrangeObjectsをアンドゥに対応させるのは自前で行なう必要があります。

 昔のアクセサメソッドと同じやりかたでrearrangeObjectsメソッドを実装して、これを呼び出す事でアンドゥ対応となります。

//
//  DragDropDataSource.m
//

- (void)rearrangeObjects
{
    [[UNDO_MANAGER prepareWithInvocationTarget:self] rearrangeObjects];
    [arrayController rearrangeObjects];
}

- (BOOL)tableView:(NSTableView*)tableView 
       acceptDrop:(id <NSDraggingInfo>)info 
              row:(int)row 
    dropOperation:(NSTableViewDropOperation)operation
{
    NSPasteboard    *pboard = [info draggingPasteboard];
    NSDragOperation dragOperation = [self tableView:tableView validateDrop:info];

    if(dragOperation == NSDragOperationCopy)
    {
        insertionRow = row;
        [Lissajous pasteFromPasteboard:pboard controller:arrayController];
        [UNDO_MANAGER setActionName:NSLocalizedString(@"dragCopyLissajous",nil)];
        return  YES;
    }
    else if(dragOperation == NSDragOperationMove)
    {
        int             i,order,index,first,last;
        NSIndexSet      *rowIndexes;
        NSManagedObject *object;
        NSArray         *arrangedObjects = [arrayController arrangedObjects];
        NSEnumerator    *enumerator = [arrangedObjects objectEnumerator];
        NSMutableArray  *orders = [NSMutableArray arrayWithCapacity:[arrangedObjects count]];

        while(object = [enumerator nextObject])
            [orders addObject:[object order]];

        rowIndexes = [NSUnarchiver unarchiveObjectWithData:[pboard dataForType:RMGRowsType]];

        //  挿入された行までのドラッグされていないオブジェクトの番号を振り直す
        first = row < [rowIndexes firstIndex] ? row : [rowIndexes firstIndex];
        if(first == 0)
            order = 0;
        else
            order = [[[arrangedObjects objectAtIndex:first-1] order] intValue] + 1;
        for(i=first;i<row;i++)
        {
            if([rowIndexes containsIndex:i])    continue;
            object = [arrangedObjects objectAtIndex:i];
            [object setValue:[orders objectAtIndex:order++] forKey:@"order"];
        }
        //  ドラッグされているオブジェクトの番号を振り直す
        index = [rowIndexes firstIndex];
        while(index != NSNotFound)
        {
            object = [arrangedObjects objectAtIndex:index];
            [object setValue:[orders objectAtIndex:order++] forKey:@"order"];
            index = [rowIndexes indexGreaterThanIndex:index];
        }
        //  挿入された行以降のドラッグされていないオブジェクトの番号を振り直す
        last = row > [rowIndexes lastIndex] ? row : [rowIndexes lastIndex];
        for(i=row;i<last;i++)
        {
            if([rowIndexes containsIndex:i])    continue;
            object = [arrangedObjects objectAtIndex:i];
            [object setValue:[orders objectAtIndex:order++] forKey:@"order"];
        }

        [self rearrangeObjects];
        [UNDO_MANAGER setActionName:NSLocalizedString(@"dragOperation",nil)];
        return YES;
    }
    else
        return  NO;
}