通知を受けて管理対象オブジェクトを並べ替える【実践的Macintoshプログラミング解説】

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

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

LinkIconホーム

更新日 2009-05-24

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

通知を受けて管理対象オブジェクトを並べ替える

 通知を受けて管理対象オブジェクトのorderを設定します。これでペーストコマンドを実行したり、ドラッグ&ドロップ操作でオブジェクトの複製を作った際に、オブジェクトが適切に並べ替えられる様になります。

 オブジェクトの選択状態の切替えや、オブジェクトが追加・削除された際のアクション名の設定もここで行います。

管理対象オブジェクトが追加、削除された時にorderを再設定する

 ここでやりたい事は管理対象オブジェクトが追加、削除されて全体の数が変わった時にorder属性を適切に設定する事です。全体の数が変わらない場合、つまりオブジェクトを移動する場合については前のページで説明しました。

 管理対象オブジェクトが追加・削除・更新された時に発行される通知を受け取る設定は既に済ませているので、ここでは通知を受けるメソッドmanagedObjectChanged:について説明します。

 一つ注意すべき点は追加、あるいは削除された管理対象オブジェクトとして通知されるのはLissajousクラスのオブジェクトだけではないという事です。OscillatorCDクラスのオブジェクトも含まれている事に留意する必要があります。

 さて、やるべき事を整理するために、まず管理対象オブジェクトが追加・削除されるケースを分類してみます。

  1. プラスボタンをクリックしてリサージュ図形を追加した
  2. マイナスボタンをクリックしてリサージュ図形を削除した
  3. 編集メニューからカット、削除コマンドを実行した
  4. 編集メニューからペーストコマンドを実行した
  5. ドラッグコピーでリサージュ図形を追加した
  6. オブジェクトの数が増減する操作のアンドゥあるいはリドゥを実行した

 1の場合は、追加されたリサージュ図形を一番最後に並べます。そして、その追加されたリサージュ図形を選択状態にして、アクション名を設定します。
 2の場合は、アクション名を設定するだけです。
 3の場合は何もしません。
 4の場合は、追加されたリサージュ図形を一番最後に並べます。そして、その追加されたリサージュ図形を選択状態にします。
 5の場合は、追加されたリサージュ図形が挿入ポイントで指示された場所にくる様に並べます。そして、その追加されたリサージュ図形を選択状態にして、アクション名を設定します。
 6の場合は追加・削除された管理対象オブジェクトを選択します。

プラスボタンをクリックしてリサージュ図形を追加した場合

 まず1の場合について説明します。通知のuserinfo辞書の中から必要な情報を取り出すのが最初にやるべき事です。

 NSInsertedObjectsKeyをキーとして追加された管理対象オブジェクトのセットを、NSDeletedObjectsKeyをキーとして削除された管理対象オブジェクトのセットを得る事ができますので、これを取り出します。

並べ替えが発生する条件 

 さて、管理対象オブジェクトの並べ替えが発生するのは1、4、5の場合です。これは

  1. 追加されたオブジェクトがある
  2. アンドゥ、リドゥ中ではない

という二つの条件が揃った場合ですので、その条件が揃った場合に並べ替えを行なう様にします。

//
//  DragDropDataSource.m
//

#pragma mark -
#pragma mark Check Managed Objects

- (void)managedObjectChanged:(NSNotification *)notification
{
    NSSet               *insertedObjects,*deletedObjects;

    insertedObjects = [[notification userInfo] objectForKey:NSInsertedObjectsKey];
    deletedObjects = [[notification userInfo] objectForKey:NSDeletedObjectsKey];

    if([insertedObjects count] > 0 && ![UNDO_MANAGER isUndoing] && ![UNDO_MANAGER isRedoing])
    {

配列コントローラが管理するオブジェクトの中から挿入されたオブジェクトを抜き出してorder順に並べる

 最初に注意を述べましたが、追加された管理対象オブジェクトとして通知されるのはLissajousクラスのオブジェクトだけではありません。OscillatorCDクラスのオブジェクトも含まれています。orderを並べ替える対象のオブジェクトはLissajousクラスのオブジェクトだけですので、追加されたLissajousクラスのオブジェクトだけを抜き出してNSMutableArrayに入れます。

//
//  DragDropDataSource.m
//

#pragma mark -
#pragma mark Check Managed Objects

- (void)managedObjectChanged:(NSNotification *)notification
{
    NSSet               *insertedObjects,*deletedObjects;
    NSEnumerator        *enumerator;
    NSManagedObject     *object;
    NSMutableArray      *insertedTargetObjects;

    insertedObjects = [[notification userInfo] objectForKey:NSInsertedObjectsKey];
    deletedObjects = [[notification userInfo] objectForKey:NSDeletedObjectsKey];

    if([insertedObjects count] > 0 && ![UNDO_MANAGER isUndoing] && ![UNDO_MANAGER isRedoing])
    {
        insertedTargetObjects = [NSMutableArray arrayWithCapacity:[insertedObjects count]];

        //  配列コントローラが管理するオブジェクトの中から挿入されたオブジェクトを抜き出す
        enumerator = [[arrayController arrangedObjects] objectEnumerator];
        while(object = [enumerator nextObject])
            if([insertedObjects containsObject:object])
                [insertedTargetObjects addObject:object];

オブジェクトのorderを設定する

 準備が済みましたので、オブジェクトを並べ替えていきます。追加されたオブジェクトが一番最後になるようにするので、まず追加されたもの以外をゼロから順番に並べ、その次に追加されたものをその続きの番号になるように並べます。

//
//  DragDropDataSource.m
//

#pragma mark -
#pragma mark Check Managed Objects

- (void)managedObjectChanged:(NSNotification *)notification
{
    int                 order = 0;
    NSSet               *insertedObjects,*deletedObjects;
    NSEnumerator        *enumerator;
    NSManagedObject     *object;
    NSMutableArray      *insertedTargetObjects;

    insertedObjects = [[notification userInfo] objectForKey:NSInsertedObjectsKey];
    deletedObjects = [[notification userInfo] objectForKey:NSDeletedObjectsKey];

    if([insertedObjects count] > 0 && ![UNDO_MANAGER isUndoing] && ![UNDO_MANAGER isRedoing])
    {
        insertedTargetObjects = [NSMutableArray arrayWithCapacity:[insertedObjects count]];

        //  配列コントローラが管理するオブジェクトの中から挿入されたオブジェクトを抜き出す
        enumerator = [[arrayController arrangedObjects] objectEnumerator];
        while(object = [enumerator nextObject])
            if([insertedObjects containsObject:object])
                [insertedTargetObjects addObject:object];

        //  挿入されたオブジェクト以外のオブジェクトのorderを設定する
        order = 0;
        enumerator = [[arrayController arrangedObjects] objectEnumerator];
        while(object = [enumerator nextObject])
        {
            if(![insertedTargetObjects containsObject:object])
                [object setValue:[NSNumber numberWithInt:order++] forKey:@"order"];
        }

        //  挿入されたオブジェクトのorderを設定する
        enumerator = [insertedTargetObjects objectEnumerator];
        while(object = [enumerator nextObject])
            [object setValue:[NSNumber numberWithInt:order++] forKey:@"order"];

        [arrayController rearrangeObjects];
    }
}

追加されたオブジェクトを選択してアクション名をセットする

 追加されたオブジェクトを選択してアクション名をセットすれば1のプラスボタンをクリックしてリサージュ図形を追加した場合の処理は終了です。

 追加されたオブジェクトを選択するのは簡単です。通知された追加されたオブジェクトのセットを配列に変換し、それを引数として配列コントローラにsetSelectedObjects:メッセージを送ればOKです。

 追加されたオブジェクトを選択するのは1、4、5だけではなく6の場合も含まれます。したがって条件Bは除き、条件Aが成立していたら実行します。

 さて、次にアクション名を設定しますが、設定するのは1、4、5、6のうち1の場合だけです。6はアンドゥマネージャのメソッドを使って判断できます。4、5の場合は既にアクション名が設定されているので、それで見分けます。その判断を入れたメソッドsetActionName:を用意しました。これは2の場合でも共通に使える様にするためです。

//
//  DragDropDataSource.m
//

#pragma mark -
#pragma mark Utility methods

- (void)setActionName:(NSString *)actionName
{
    if([UNDO_MANAGER isUndoing] || [UNDO_MANAGER isRedoing])
        return;

    if(![[UNDO_MANAGER undoActionName] isEqualToString:@""])
        return;

    [UNDO_MANAGER setActionName:NSLocalizedString(actionName,nil)];
}

#pragma mark -
#pragma mark Check Managed Objects

- (void)managedObjectChanged:(NSNotification *)notification
{
    int                 order = 0;
    NSSet               *insertedObjects,*deletedObjects;
    NSEnumerator        *enumerator;
    NSManagedObject     *object;
    NSMutableArray      *insertedTargetObjects;

    insertedObjects = [[notification userInfo] objectForKey:NSInsertedObjectsKey];
    deletedObjects = [[notification userInfo] objectForKey:NSDeletedObjectsKey];

    if([insertedObjects count] > 0 && ![UNDO_MANAGER isUndoing] && ![UNDO_MANAGER isRedoing])
    {
        insertedTargetObjects = [NSMutableArray arrayWithCapacity:[insertedObjects count]];

        //  配列コントローラが管理するオブジェクトの中から挿入されたオブジェクトを抜き出す
        enumerator = [[arrayController arrangedObjects] objectEnumerator];
        while(object = [enumerator nextObject])
            if([insertedObjects containsObject:object])
                [insertedTargetObjects addObject:object];

        //  挿入されたオブジェクト以外のオブジェクトのorderを設定する
        order = 0;
        enumerator = [[arrayController arrangedObjects] objectEnumerator];
        while(object = [enumerator nextObject])
        {
            if(![insertedTargetObjects containsObject:object])
                [object setValue:[NSNumber numberWithInt:order++] forKey:@"order"];
        }

        //  挿入されたオブジェクトのorderを設定する
        enumerator = [insertedTargetObjects objectEnumerator];
        while(object = [enumerator nextObject])
            [object setValue:[NSNumber numberWithInt:order++] forKey:@"order"];

        [arrayController rearrangeObjects];
    }
    if([insertedObjects count] > 0)
    {
        [arrayController setSelectedObjects:[insertedObjects allObjects]];
        [self setActionName:@"addLissajous"];
    }
}

ペースト時に選択するコードを削除する

 RMGRootのpasteFromPasteboard:controller:メソッドで、最後にペーストしたオブジェクトを選択するコードを書いていました。管理対象オブジェクトコンテキストに追加するタイミングと配列コントローラのarrangedObjectsに追加されるタイミングが合わないので、わざわざ遅延実行までして実現していたのですが、通知を受けたタイミングで選択すれば何の問題もありません。

 したがってpasteFromPasteboard:controller:メソッドのコードを削除する事にします。

2008/9/28 選択用のNSMutableArrayも不要になったので、合わせて削除しました。

+ (void)pasteFromPasteboard:(NSPasteboard *)pboard
                 controller:(NSArrayController *)controller
{
    NSManagedObject     *object;
    NSArray             *dictionarys;
    NSDictionary        *dictionary;
    NSEnumerator        *enumerator;
    NSMutableArray      *objects;
    NSData              *data = [pboard dataForType:[[self class] dataType]];

    if(data == nil)     return;

    dictionarys = [NSUnarchiver unarchiveObjectWithData:data];
    enumerator = [dictionarys objectEnumerator];
    objects = [NSMutableArray arrayWithCapacity:[dictionarys count]];
    while(dictionary = [enumerator nextObject])
    {
        object = [NSEntityDescription insertNewObjectForEntityForName:[controller entityName]
                                               inManagedObjectContext:[controller managedObjectContext]];
        [object setValuesForKeysWithDictionary:dictionary];
        [objects addObject:object];
    }
    [controller performSelector:@selector(setSelectedObjects:)
                     withObject:objects
                     afterDelay:0];
}

マイナスボタンをクリックしてリサージュ図形を削除した場合

 2の場合はアクション名を設定するだけですので、上で説明したメソッドを使います。削除されたオブジェクトがあると通知された場合にsetActionName:でアクション名をセットします。

//
//  DragDropDataSource.m
//

#pragma mark -
#pragma mark Utility methods

- (void)setActionName:(NSString *)actionName
{
    if([UNDO_MANAGER isUndoing] || [UNDO_MANAGER isRedoing])
        return;

    if(![[UNDO_MANAGER undoActionName] isEqualToString:@""])
        return;

    [UNDO_MANAGER setActionName:NSLocalizedString(actionName,nil)];
}

#pragma mark -
#pragma mark Check Managed Objects

- (void)managedObjectChanged:(NSNotification *)notification
{
    int                 order = 0;
    NSSet               *insertedObjects,*deletedObjects;
    NSEnumerator        *enumerator;
    NSManagedObject     *object;
    NSMutableArray      *insertedTargetObjects;

    insertedObjects = [[notification userInfo] objectForKey:NSInsertedObjectsKey];
    deletedObjects = [[notification userInfo] objectForKey:NSDeletedObjectsKey];

    if([insertedObjects count] > 0 && ![UNDO_MANAGER isUndoing] && ![UNDO_MANAGER isRedoing])
    {
        insertedTargetObjects = [NSMutableArray arrayWithCapacity:[insertedObjects count]];

        //  配列コントローラが管理するオブジェクトの中から挿入されたオブジェクトを抜き出す
        enumerator = [[arrayController arrangedObjects] objectEnumerator];
        while(object = [enumerator nextObject])
            if([insertedObjects containsObject:object])
                [insertedTargetObjects addObject:object];

        //  挿入されたオブジェクト以外のオブジェクトのorderを設定する
        order = 0;
        enumerator = [[arrayController arrangedObjects] objectEnumerator];
        while(object = [enumerator nextObject])
        {
            if(![insertedTargetObjects containsObject:object])
                [object setValue:[NSNumber numberWithInt:order++] forKey:@"order"];
        }

        //  挿入されたオブジェクトのorderを設定する
        enumerator = [insertedTargetObjects objectEnumerator];
        while(object = [enumerator nextObject])
            [object setValue:[NSNumber numberWithInt:order++] forKey:@"order"];
        insertionRow = -1;

        [arrayController rearrangeObjects];
    }
    if([insertedObjects count] > 0)
    {
        [arrayController setSelectedObjects:[insertedObjects allObjects]];
        [self setActionName:@"addLissajous"];
    }
    if([deletedObjects count] > 0)
    {
        [self setActionName:@"deleteLissajous"];
    }
}

編集メニューからカット、削除、ペーストコマンドを実行した場合

 3の場合は何もしませんのでとばします。
 4の場合にやる事は1の場合とほとんど同じで、違いは最後にアクション名をセットしない事だけです。setActionName:メソッドで、アクション名が既にセットされている場合は新たにアクション名をセットしない様にしているので、4の場合にやるべき事は既に達成されています。

ドラッグコピーでオブジェクトを追加した場合

 5の場合は追加されたオブジェクトが挿入ポイントで指示された場所にくる様に並べます。ここでtableView:acceptDrop:row:dropOperation:メソッドで設定しておいたインスタンス変数insertionRowを使います。

必要に応じてinsertionRowを初期設定する

  •  5の場合はinsertionRowが設定されるのですが、5以外の場合はされないのでこれまでと同じ動作、つまりオブジェクトを最後の行に追加する事になる様に、insertionRowを設定します。
  •  見分け方ですが、このメソッドの終わりでinsertionRowが負の数になる様にリセットするので、insertionRowが負なら5以外の場合という事です。その場合はinsertionRowを(全オブジェクト数 - 挿入されたオブジェクト数)とします。

場所を空ける処理を追加する

  •  insertionRowの位置に挿入されたオブジェクトが来るので、挿入されたオブジェクト以外のオブジェクトのorderを設定している時にorderがinsertionRowに達したら、必要な分orderを飛ばす処理を追加します。飛ばす数は追加されたオブジェクトの数です。

insertionRowに並べる

  •  あとは挿入されたオブジェクトのorderを設定する際、初期値をinsertionRowにする処理を追加して、最後にinsertionRowを負の数にリセットすれば完了です。

//
//  DragDropDataSource.m
//

#pragma mark -
#pragma mark Utility methods

- (void)setActionName:(NSString *)actionName
{
    if([UNDO_MANAGER isUndoing] || [UNDO_MANAGER isRedoing])
        return;

    if(![[UNDO_MANAGER undoActionName] isEqualToString:@""])
        return;

    [UNDO_MANAGER setActionName:NSLocalizedString(actionName,nil)];
}

#pragma mark -
#pragma mark Check Managed Objects

- (void)managedObjectChanged:(NSNotification *)notification
{
    int                 order = 0;
    NSSet               *insertedObjects,*deletedObjects;
    NSEnumerator        *enumerator;
    NSManagedObject     *object;
    NSMutableArray      *insertedTargetObjects;

    insertedObjects = [[notification userInfo] objectForKey:NSInsertedObjectsKey];
    deletedObjects = [[notification userInfo] objectForKey:NSDeletedObjectsKey];

    if([insertedObjects count] > 0 && ![UNDO_MANAGER isUndoing] && ![UNDO_MANAGER isRedoing])
    {
        insertedTargetObjects = [NSMutableArray arrayWithCapacity:[insertedObjects count]];

        //  配列コントローラが管理するオブジェクトの中から挿入されたオブジェクトを抜き出す
        enumerator = [[arrayController arrangedObjects] objectEnumerator];
        while(object = [enumerator nextObject])
            if([insertedObjects containsObject:object])
                [insertedTargetObjects addObject:object];
        if(insertionRow < 0)
            insertionRow = [[arrayController arrangedObjects] count] - [insertedTargetObjects count];

        //  挿入されたオブジェクト以外のオブジェクトのorderを設定する
        order = 0;
        enumerator = [[arrayController arrangedObjects] objectEnumerator];
        while(object = [enumerator nextObject])
        {
            if(order == insertionRow)
                order += [insertedTargetObjects count];
            if(![insertedTargetObjects containsObject:object])
                [object setValue:[NSNumber numberWithInt:order++] forKey:@"order"];
        }

        //  挿入されたオブジェクトのorderを設定する
        order = insertionRow;
        enumerator = [insertedTargetObjects objectEnumerator];
        while(object = [enumerator nextObject])
            [object setValue:[NSNumber numberWithInt:order++] forKey:@"order"];
        insertionRow = -1;

        [arrayController rearrangeObjects];
    }
    if([insertedObjects count] > 0)
    {
        [arrayController setSelectedObjects:[insertedObjects allObjects]];
        [self setActionName:@"addLissajous"];
    }
    if([deletedObjects count] > 0)
    {
        [self setActionName:@"deleteLissajous"];
    }
}

オブジェクトの数が増減する操作のアンドゥあるいはリドゥを実行した場合

 6の場合は追加・削除された管理対象オブジェクトを選択します。追加された管理対象オブジェクトを選択するのは簡単で、既にその機能は実装しています。問題はアンドゥ、もしくはリドゥ操作によってオブジェクトが削除された場合、どのオブジェクトを選択すべきかという事です。

 例えば二番目のオブジェクトを選択している状態でペーストコマンドを実行したとします。オブジェクトが行の最後に追加され、それらが選択されます。この状態でアンドゥするとペーストされたオブジェクトが削除されます。この時にどのオブジェクトを選択すべきか?
 何も手を打たなければ一番最初の行が選択されます。これでは不自然なので、できれば二番目のオブジェクトを選択している状態に戻したいところです。

 かなり細かい話かなという気もします。そこまでこだわらなくてもよい場合は、実装を省いても構わないでしょう。

 対策として選択履歴を保持する事にします。インターフェイスファイルに履歴保持用のインスタンス変数を追加します。二世代分必要なので、二つ用意します。

//
//  DragDropDataSource.h
//  RepeatingMotifGenerator
//
//  Copyright NovemberKou 2008 . All rights reserved.
//

#import <Cocoa/Cocoa.h>


@interface DragDropDataSource : NSObject
{
    IBOutlet    NSArrayController   *arrayController;

    int         insertionRow;
    NSIndexSet  *selectionIndexes,*oldSelectionIndexes;
}

@end

 awakeFromNibに配列コントローラのselectionIndexesを監視する設定を追加します。deallocメソッドでは監視に使ったインスタンス変数をreleaseします。そして監視するプロパティのアクセサメソッドを追加します。

#pragma mark -
#pragma mark Initializer and Deallocator

- (void)awakeFromNib
{
    NSNotificationCenter    *nc = [NSNotificationCenter defaultCenter];

    insertionRow = -1;
    [nc addObserver:self
           selector:@selector(managedObjectChanged:)
               name:NSManagedObjectContextObjectsDidChangeNotification
             object:[arrayController managedObjectContext]];

    selectionIndexes = oldSelectionIndexes = nil;
    [self bind:@"selectionIndexes"
      toObject:arrayController
   withKeyPath:@"selectionIndexes"
       options:nil];
}

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [selectionIndexes release];
    [oldSelectionIndexes release];
    [super dealloc];
}

#pragma mark -
#pragma mark Accessor methods

- (NSIndexSet *)selectionIndexes    {return  selectionIndexes;}
- (void)setSelectionIndexes:(NSIndexSet *)newSelection
{
    if(![UNDO_MANAGER isUndoing] && ![UNDO_MANAGER isRedoing])
    {
        [newSelection retain];
        [oldSelectionIndexes release];
        oldSelectionIndexes = selectionIndexes;
        selectionIndexes = newSelection;
    }
}

 そして削除されたオブジェクトがあってアンドゥ中の場合は、二世代前の選択履歴を使って選択状態を元に戻します。

 今のはアンドゥによってオブジェクトが削除された場合の動きの話でした。ではリドゥによってオブジェクトが削除された場合はどうするのか?リドゥの場合は最初の行が選択されるのですが、これは不自然な動作ではないと感じました。したがって特に何か対策する必要はありません。

//
//  DragDropDataSource.m
//

#pragma mark -
#pragma mark Utility methods

- (void)setActionName:(NSString *)actionName
{
    if([UNDO_MANAGER isUndoing] || [UNDO_MANAGER isRedoing])
        return;

    if(![[UNDO_MANAGER undoActionName] isEqualToString:@""])
        return;

    [UNDO_MANAGER setActionName:NSLocalizedString(actionName,nil)];
}

#pragma mark -
#pragma mark Check Managed Objects

- (void)managedObjectChanged:(NSNotification *)notification
{
    int                 order = 0;
    NSSet               *insertedObjects,*deletedObjects;
    NSEnumerator        *enumerator;
    NSManagedObject     *object;
    NSMutableArray      *insertedTargetObjects;

    insertedObjects = [[notification userInfo] objectForKey:NSInsertedObjectsKey];
    deletedObjects = [[notification userInfo] objectForKey:NSDeletedObjectsKey];

    if([insertedObjects count] > 0 && ![UNDO_MANAGER isUndoing] && ![UNDO_MANAGER isRedoing])
    {
        insertedTargetObjects = [NSMutableArray arrayWithCapacity:[insertedObjects count]];

        //  配列コントローラが管理するオブジェクトの中から挿入されたオブジェクトを抜き出す
        enumerator = [[arrayController arrangedObjects] objectEnumerator];
        while(object = [enumerator nextObject])
            if([insertedObjects containsObject:object])
                [insertedTargetObjects addObject:object];
        if(insertionRow < 0)
            insertionRow = [[arrayController arrangedObjects] count] - [insertedTargetObjects count];

        //  挿入されたオブジェクト以外のオブジェクトのorderを設定する
        order = 0;
        enumerator = [[arrayController arrangedObjects] objectEnumerator];
        while(object = [enumerator nextObject])
        {
            if(order == insertionRow)
                order += [insertedTargetObjects count];
            if(![insertedTargetObjects containsObject:object])
                [object setValue:[NSNumber numberWithInt:order++] forKey:@"order"];
        }

        //  挿入されたオブジェクトのorderを設定する
        order = insertionRow;
        enumerator = [insertedTargetObjects objectEnumerator];
        while(object = [enumerator nextObject])
            [object setValue:[NSNumber numberWithInt:order++] forKey:@"order"];
        insertionRow = -1;

        [arrayController rearrangeObjects];
    }
    if([insertedObjects count] > 0)
    {
        [arrayController setSelectedObjects:[insertedObjects allObjects]];
        [self setActionName:@"addLissajous"];
    }
    if([deletedObjects count] > 0)
    {
        if([UNDO_MANAGER isUndoing])
            [arrayController setSelectionIndexes:oldSelectionIndexes];
        [self setActionName:@"deleteLissajous"];
    }
}