ラベル Cocoa Bindings の投稿を表示しています。 すべての投稿を表示
ラベル Cocoa Bindings の投稿を表示しています。 すべての投稿を表示

[Mac] NSArrayController に Undo/Redo を実装する [6] ソート指定のUndo/Redo

2011年1月4日火曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: [Mac] NSArrayController に Undo/Redo を実装する [5] 選択のUndo/Redo

次はソートの Undo/Redo。


ソートの Undo/Redo


NSTableView でカラムヘッダをクリックするとソート処理が行われる。この時に setSortDescriptors: が呼ばれるのでこれをオーバーライドしてやればいい。
- (void)setSortDescriptors:(NSArray *)sortDescriptors
{
    [[self.undoManagerprepareWithInvocationTarget:self]
        setSortDescriptors:[selfsortDescriptors]];
    [supersetSortDescriptors:sortDescriptors];
}

できた。図じゃわかりずらいが。

ソースコード


GitHubからどうぞ。

ArrayControllerUndoSample at 2011-01-04 from xcatsan/MacOSX-Sample-Code - GitHub

[Mac] NSArrayController に Undo/Redo を実装する [5] 選択のUndo/Redo

2011年1月3日月曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: [Mac] NSArrayController に Undo/Redo を実装する [4] 初期値ありの場合

選択状態も Undo/Redo してみる。

選択状態の Undo/Redo


NSArrayController の選択に関係するメソッドをすべてオーバーライドしてやる。
- (void)_registSelectionToUndo
{
    [[self.undoManagerprepareWithInvocationTarget:self]
      setSelectionIndexes:[selfselectionIndexes]];

}
- (BOOL)setSelectionIndex:(NSUInteger)index
{
    BOOL result = [supersetSelectionIndex:index];
    if (result) {
        [self_registSelectionToUndo];
    }
    return result;
}

- (BOOL)setSelectionIndexes:(NSIndexSet *)indexes
{
    BOOL result =  [supersetSelectionIndexes:indexes];
    if (result) {
        [self_registSelectionToUndo];
    }
    return result;
}

- (BOOL)setSelectedObjects:(NSArray *)objects
{
    BOOL result =  [supersetSelectedObjects:objects];
    if (result) {
    [self_registSelectionToUndo];
    }
    return result;
}

- (BOOL)addSelectedObjects:(NSArray *)objects
{
    BOOL result = [superaddSelectedObjects:objects];
    if (result) {
        [self_registSelectionToUndo];
    }
    return result;
}

- (void)selectPrevious:(id)sender
{
    if ([selfcanSelectPrevious]) {
        [self_registSelectionToUndo];
    }
    [superselectPrevious:sender];
}

- (void)selectNext:(id)sender
{
    if ([selfcanSelectNext]) {
        [self_registSelectionToUndo];
    }
    [superselectNext:sender];
}

できた(画像じゃわかりずらいが)

ソースコード


GitHubからどうぞ。

ArrayControllerUndoSample at 2011-01-03 from xcatsan/MacOSX-Sample-Code - GitHub

[Mac] NSArrayController に Undo/Redo を実装する [4] 初期値ありの場合

2011年1月2日日曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: [Mac] NSArrayController に Undo/Redo を実装する [2] カスタムモデル

初期値が存在する場合、前回までのコードでは対応できない。

初期値ありへの対応


これまでのアーキテクチャではカラムの値を KVO 登録擦る必要があり、これをレコード挿入時に行っていた。ところが初期値ありの場合、レコード挿入のメソッドは呼ばれない。代わりに setContent: が呼ばれるのでこれをオーバーライドしてやる。
- (void)setContent:(id)content
{
    [self_addObserverFor:content];
    [supersetContent:content];
}

できた(画像ではわからないが)。


ソースコード


GitHubからどうぞ。

ArrayControllerUndoSample at 2011-01-02 from xcatsan/MacOSX-Sample-Code - GitHub

[Mac] NSArrayController に Undo/Redo を実装する [3] 改良

2010年12月24日金曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: [Mac] NSArrayController に Undo/Redo を実装する [2] カスタムモデル

前回のコードを改良した。


改良点


前回はレコードの挿入毎にプロパティの一覧を取得していた。NSArrayController 初期化時にモデルのクラスは Nibからわかるのでこのタイミングで KVO監視対象のリストを作ってしまうようにした。
- (void)_setup
{
 self.undoManager = [[[NSUndoManager alloc] init] autorelease];
 self.keys = [self _propertyListOfClass:[self objectClass]];
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
 self = [super initWithCoder:aDecoder];
 if (self != nil) {
  [self _setup];
 }
 return self;
}
これに伴い挿入処理でプロパティリストを取得する処理が不要になった。

またプログラミングで setObjectClass: が呼ばれた場合を想定してこれをオーバーライドしておく。
{
 [super setObjectClass:objectClass];
 self.keys = [self _propertyListOfClass:objectClass];
}


ソースコード


GitHub からどうぞ。
ArrayControllerUndoSample at 2010-12-24b from xcatsan/MacOSX-Sample-Code - GitHub

[Mac] NSArrayController に Undo/Redo を実装する [2] カスタムモデル

2010年12月23日木曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: [Mac] NSArrayController に Undo/Redo を実装する

モデルのクラスを NSMutableDictionary ではなくカスタムモデル(クラス)を使うケースを考えてみた。


カスタムモデル


こんなクラスを定義してみた。
@interface Book : NSObject {

 NSString* titile_;
 NSString* author_;
}
@property (nonatomic, copy) NSString* title;
@property (nonatomic, copy) NSString* author;

@end
NSMutableDictionary と同等の働きを持つが、値の管理をプロパティで行っている。NSArrayController にモデルのクラスを伝えるには -[NSObjectControlelr setObjectClass:]を使う。
NSObjectController Class Reference

Interface Builder を使えばインスペクタでクラス名を指定できる。


プロパティから監視キーを自動取得する


プロパティはランタイム関数 class_copyPropertyList を使うと特定のクラスで定義されているプロパティ名のリストを取得することができる。これは以前紹介したことがある。

[参照] (旧) Cocoaの日々: @property の一覧を取得する

これを使って KVO登録に必要なキー名の一覧を取得してみた。
- (NSArray*)_propertyListOf:(id)object
{
 NSMutableArray* list = [NSMutableArray array];
 unsigned int outCount, i;
 objc_property_t *properties = class_copyPropertyList([object class], &outCount);
 
 for(i = 0; i < outCount; i++) {
  objc_property_t property = properties[i];
  const char *propName = property_getName(property);
  NSString *propertyName = [NSString stringWithUTF8String:propName];
  [list addObject:propertyName];
 }
 free(properties);
 return list;
}
これを使い KVO登録を行う。
- (void)_addObserverFor:(NSArray*)objects
{
 for (id object in objects) {
  NSArray* keys = self.keys;
  if (!keys) {
   keys = [self _propertyListOf:object];
  }
  for (NSString* key in keys) {
   [object addObserver:self
      forKeyPath:key
      options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld
      context:nil];   
  }
 }
}
結果はOK。前回のようにわざわざ監視キーの一覧を渡さなくて良いのでこちらの方が使い勝手が良い。自分の場合、サンプルを除けば NSMutableDictionary をモデルとして使うことはあまり無くて、どちらかといえば手間をかけてカスタムクラスを定義する方なのでこちらで十分使えそうだ。


備考


前回の初期化コードにはバグがあって Interface Builder で定義した内容が実行時に反映されていなかった。
- (id)initWithCoder:(NSCoder *)aDecoder
{
 // self = [super init];  ※間違い
 self = [super initWithCoder:aDecoder];
 if (self != nil) {
  [self _setup];
 }
 return self;
}
Nib 経由でインスタンス化される場合は initWithCoder: が呼ばれるが、そこで初期化処理を書く場合は親の initWithCoder: を呼ぶ必要がある。それによって Nibで定義した情報を元にインスタンス化されるのだが、前回のように initを読んでしまうと Nibとは無関係に普通に初期化してしまう。


ソースコード


GitHub からどうぞ。 ArrayControllerUndoSample at 2010-12-23 from xcatsan/MacOSX-Sample-Code - GitHub

[Mac] NSArrayController に Undo/Redo を実装する

2010年12月22日水曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

NSArrayController に Undo/Redo を実装してみた。

サンプル


NSTableView → NSArrayController → NSMutableArray[NSMutableDictionary] と繋いだ簡単なサンプルを作った。

NSTableView上での挿入・削除・値変更について Undo/Redo が利用できる。


アーキテクチャ


Undo/Redo の処理を MVC の C、すなわちコントローラで実装した。

Undo/Redoの機能は本来モデルで実装すべき処理かもしれない。Core Data では実際そうしている。ただコントローラで Undo/Redo 機能があると特別なモデルクラス(例えば NSManagedObject)を用意する必要がないというメリットがある。つまり普通のモデルクラスに後からコントローラ側で Undo/Redo機能を追加できる。

今回は NSArrayController のサブクラスを作り、その内部で独自の NSUndoManager を管理するようにした。


実装


今回作成したクラス CustomArrayController のインターフェイス。
@interface CustomArrayController : NSArrayController {

 NSUndoManager* undoManager_;
 BOOL skipFlag_;
 NSArray* keys_;
}
@property (nonatomic, retain, readonly) NSUndoManager* undoManager;
@property (nonatomic, retain) NSArray* keys;

-(void)undo;
-(void)redo;

-(IBAction)undo:(id)sender;
-(IBAction)redo:(id)sender;

@end
NSUndoManager は他のUndoと統合(グループ化)したり外部から操作できるようにプロパティ定義した。keys は値監視対象のキー名を格納する。

実装のポイントはレコードの操作の Undo/Redo処理と、カラム値の Undo/Redo処理。

レコード操作の Undo/Redo処理


これはNSArrayController の下記のメソッドをオーバライドし、そこへ Undo/Redo処理を実装した。
- (void)insertObject:(id)object atArrangedObjectIndex:(NSUInteger)index
- (void)insertObjects:(NSArray *)objects atArrangedObjectIndexes:(NSIndexSet *)indexes
- (void)removeObjectAtArrangedObjectIndex:(NSUInteger)index
- (void)removeObjectsAtArrangedObjectIndexes:(NSIndexSet *)indexes
挿入された時には削除操作をNSUndoManagerへ登録し、反対に削除された時は挿入操作を登録する処理を書く。例えば -insertObject:atArrangedObjectIndex: の実装はこんな感じ。
if (!skipFlag_) {
  [self _addObserverFor:[NSArray arrayWithObject:object]];

  [[self.undoManager prepareWithInvocationTarget:self]  // Undo登録
   removeObjectAtArrangedObjectIndex:index];  
 }
 [super insertObject:object atArrangedObjectIndex:index];
skipFlag_ は2重処理を防ぐために入れてある。複数のオブジェクト挿入を行う insertObjects:atArrangedObjectIndexes: は insertObject:atArrangedObjectIndex: を内部的に呼び出す。今回は insertObjects:atArrangedObjectIndexes: 側にも Undo登録の処理を書いているので、そちらと処理が重ならないようにこのような判定を行っている。-insertObjects:atArrangedObjectIndexes: の実装はこう。
- (void)insertObjects:(NSArray *)objects atArrangedObjectIndexes:(NSIndexSet *)indexes
{
 [self _addObserverFor:objects];

 [[self.undoManager prepareWithInvocationTarget:self]
  removeObjectsAtArrangedObjectIndexes:indexes];

 skipFlag_ = YES;
 [super insertObjects:objects atArrangedObjectIndexes:indexes];
 skipFlag_ = NO;
}

今回の検証でわかったのだが1件だけ削除するケースでは(複数削除)→(単一削除)という処理が走る。しかし複数件削除するケースでは単一削除が呼ばれない。
a) 1件削除の場合
removeObjectsAtArrangedObjectIndexes:
 ↓
removeObjectAtArrangedObjectIndex:

b) 複数件削除の場合
removeObjectsAtArrangedObjectIndexes:
複数削除のメソッドから必ず単一削除のメソッドを呼び出すのなら Undo登録は単一削除側だけで良いのだが、この変則的な呼び出しルールがある為、skipFlag_ を導入した。挿入処理だとルールがまた違ったのだが削除側と対称性を持たせるために同じ処理を書いた。


カラム値の Undo/Redo処理


カラム値の Undo/Redo処理を実装するには、NSArrayController のバインド先のモデルの値の変化イベントとその値が必要となる。今回はレコード挿入時に各カラム値をKVO登録してその変化を取得するようにした。先ほどの挿入コード内で呼んでいた _addObserverFor: がその処理。
- (void)_addObserverFor:(NSArray*)objects
{
 for (id object in objects) {
  NSArray* keys = self.keys;
  for (NSString* key in keys) {
   [object addObserver:self
      forKeyPath:key
      options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld
      context:nil];   
  }
 }
}
self.keys はサンプルだと {@"title", @"author"} という配列が入っている。この配列は外部から与えている。本来はモデルからこのキー名を取得できれば良いのだが NSMutableDictionary を NSArrayControllerのモデル(レコード)として使っている場合、レコード新規作成時にはこの情報が取れない。カラムの値(例えば title)を登録した後になって -[NSMutableDictionary allKeys] でキー名が取れるようになる。この為、外部から監視対象のキー名を与えるようになっている。
なお -[NSObject addOberser:forKeyPath:options:context:] に forKeyPath:@"self" を指定した場合は変更イベントを受け取れなかった。

一旦 KVO登録できれば監視対象のカラムの変更イベントをうけとれる。実装はこんな感じ。
- (void)observeValueForKeyPath:(NSString *)keyPath
       ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
 if (change) {
  id value = [change objectForKey:NSKeyValueChangeOldKey];
  if (value == [NSNull null]) {
   value = nil;
  }
  [[self.undoManager prepareWithInvocationTarget:self]
   _setArrangedObject:object value:value forKeyPath:keyPath];   
 }
}
監視対象の値の変化があった時に observeValueForKeyPath:ofObject:change:context: が呼ばれる。変化内容は changeに入っていてこんな感じで渡ってくる。
change: {
    kind = 1;
    new = B;
    old = A;
}
値が nil の場合、NSNull のインスタンスが渡ってくるのでその変換を行っておく。NSUndoManager には _setArrangedObject:value:forKeyPath: を登録しておく。
- (void)_setArrangedObject:(id)object value:(id)value forKeyPath:(NSString*)keyPath
{
 id currentValue = [object valueForKey:keyPath];
 
 [[self.undoManager prepareWithInvocationTarget:self]
  _setArrangedObject:object value:currentValue forKeyPath:keyPath]; 
 
 [object setValue:value forKeyPath:keyPath];
}
このメソッドは現在値を取得して、その値を使って自分自身のメソッドを呼び出すように NSUndoManager へ登録する。こうすると Undo時にもこのメソッドが呼ばれ、その時には Redo用の登録が行われることになる(Undo/Redoの判断は NSUndoManagerが把握していてるので気にする必要はない)。その後モデルのカラム値を setValue:forKeyPath: で設定する。

なおレコードが削除される時には KVOを解除しておく。レコード削除コードでは下記のメソッドを呼び出しておく。
- (void)_removeObserverFor:(NSArray*)objects
{
 for (id object in objects) {
  NSArray* keys = self.keys;
  for (NSString* key in keys) {
   [object removeObserver:self
      forKeyPath:key];
  }
 }
 
}
削除後も NSUndoManager によってレコードのインスタンスは retain されていて、Undo時にまた同じインスタンスがモデル配列へ追加される。この時には NSUndoManagerから通常の操作と同じく insertObject:atArrangedObjectIndex: が呼ばれるため、そこでまた KVO登録処理が走る。この為、Undo/Redoを繰り返すと同じ KVO登録が複数行われ、試した時にはアプリの動作が止まったように遅くなってしまった。これを防ぐために(またリソースを無駄にしない為にも)KVO解除を忘れずにやっておく。


[参考]Cocoaの日々: [Mac] NSArrayController の KVO通知の中身が nil になる(現象記録)


課題


カラムのキー名を外部から与えるというのがいかにもカッコ悪いし使いづらい。Interface Builder で NSArrayControllerのインスペクタを見るとキー名の表示があるのでこれが取得できれば良いのだが方法がわからなかった。ここに表示されるということは Nib内にあるハズなのだが。



ソースコード


GitHub からどうぞ。

ArrayControllerUndoSample at 2010-12-22 from xcatsan/MacOSX-Sample-Code - GitHub


参考情報


(旧) Cocoaの日々: rubberBand(その19)Undoの実装 / NSUndoManager
昔書いた NSUndoManager の説明。

NSUndoManager Class Reference

Key-Value Observing Programming Guide: Introduction to Key-Value Observing Programming Guide

http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/CocoaBindings/Concepts/HowDoBindingsWork.html%23//apple_ref/doc/uid/20002373-1…

NSArrayController Class Reference

[Mac] NSArrayController の KVO通知の中身が nil になる(現象記録)

2010年12月20日月曜日 | Published in | 2 コメント

このエントリーをはてなブックマークに追加

NSTableView→NSArrayController→NSMutableArray とバインドしている状態でテーブルのカラムの値の変化を NSArrayControllerのKVO経由で取得してみた。

KVOの登録はこんな感じ。
[_userArrayController addObserver:self
         forKeyPath:@"arrangedObjects.title"
         options:NSKeyValueObservingOptionOld|
         NSKeyValueObservingOptionNew|
         NSKeyValueObservingOptionInitial|
         NSKeyValueObservingOptionPrior
         context:nil];
カラムの編集前後で下記が呼び出される。
- (void)observeValueForKeyPath:(NSString *)keyPath
       ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
        :
}
change を見ると変更前後の値が共に nil になっていて値が取得できなかった。
[81129:a0f] change={
    kind = 1;
    new = <null>;
    old = <null>;
}

既知の問題なのか仕様なのかわからない。ネットで探すと同じ現象が報告されていた。
cocoa - Observer properties of objects in an NSArrayController - Stack Overflow
forKeyPath:@"arrangedObjects.name" 
forKeyPath:@"contentArray.name" 
forKeyPath:@"content.name" 
forKeyPath:@"selection.name" 
forKeyPath:@"selectedObjects.name"
いろんな keyPathを試しているようだが駄目っぽい。

KVO for NSArrayController subclass
ずいぶん昔からあるようなので仕様なのか?

- - - -
NSArrayController を対象に Undo機能を実装しようとしているが KVOが使えないので困っている。仕方がないので NSArrayController のサブクラスを作り対応しようとしているが今度は objectDidBeginEditing: の引数(実体が非公開のNSTextValueBinder)の扱いで困っている。編集前後のイベントは取れるが値が取得できない。うーむ。

NSArrayController を使った NSTableView で選択行の情報を取得する

2010年12月2日木曜日 | Published in | 1 コメント

このエントリーをはてなブックマークに追加

NSArrayController でソート(SortDescriptors)を使っている場合の選択行の取得方法について。

-[NSTableViewDeleage tableView:shouldSelectRow:] で得た NSTableView上の選択位置 row を使って単純にモデル配列内の選択行を取り出す場合はこんな感じになる。
id obj = [modelArray objectAtIndex:row];
これは画面上でソートしている場合間違った行情報を取得することになる。
イメージ
NSTableView
 ↑
NSArrayController(ソートされている)
 ↓
モデル配列
画面上はソートした一覧が表示されていて、その並び順を元に選択位置 row を NSTableViewDelegateで通知する。つまり、
画面上:
 0:バナナ
 1:りんご
 2:苺

モデル配列:
 0:苺
 1:バナナ
 2:りんご
のようなケースが有りえて画面で「バナナ」を選択した場合 row=0 となるが、それをそのままモデル配列に当てはめると「苺」をさ指定しまう。


なので NSTableViewDelegate の受け側では渡された row を元に情報を取得する場合、オリジナルのモデル配列を使うのではなく画面上の並びに一致する NSArrayController のオブジェクトから情報を取得する必要がある。

NSArrayControlelr からの選択行の取り出しには -[arrangedObjects] を使う。これで取得した配列に対して objectAtIndex: を投げて目的の行情報を取得する。
id obj = [[arrayController arrangedObjects] objectAtIndex:row];

なお NSArrayController には選択行向けのメソッドが用意されている。
NSArrayController Class Reference
親クラスの NSObjectController にもある。
NSObjectController Class Reference

が、これらを使って意図通りの情報を取得することができなかった。使い方が悪いのか、設定が不足しているのか。もう少し調べる必要あり。

人気の投稿(過去 30日間)