Cocoa NSUndoManager (REDO/UNDO)

Cocoa NSUndoManagersg_trans.gif (REDO/UNDO)

原文: http://blog.sina.com.cn/s/blog_5df7dcaf0100bp8w.html

 

NSUndoManager

 

使用NSUndoManaer, 们可以给程序以一种优雅的风格添加undo功能. undo管理器跟踪管理一个对象的添加,编辑和删除.这些消息将会发送undo管理器去做undo. 而当我们请求做undo操作, undo管理器也会跟踪这些消息,这些消息会被记录用来做redo. 该机制使用两个NSInvocation对像堆栈来实现.

 

这么早就讨论这个主题是相当沉重的.(时候一说起undo.我的头就有点大.),过因undodocument,所以我们先来学习undo是怎么工作的.这样在下一章能更好理解document构的工作流程.

 

NSInvocation
正如你所想, 应该有个对象能方便的封装一个消息[就是一个操作] - 包含selector, 接受对象,以及所有的参数 . NSInvocation对象就是这样的对象.

invocation一个非常方便的用途就是转发消息. 当一个对象接受到一个它没法响应的消息[没有现该方法].message-sending统不会马上抛出一个异常,它会先检查该对象是否实现了这个方法
- (void)forwardInvocation:(NSInvocation *)x
如果对象实现了该方法.那么这个消息就会被封包成对象NSInvocation-x.调用forwardInvocation:方法

NSUndoManager是怎样工作的
假定一下用户打开一个新的RaiseMan document,并且做了3编辑动作
.添加一条记录
.记录的名字"New Employee" 修改"Rex Fido"
.raise改成20
现每一次修改时,controller将把一个要做undoinvocation添加到undo栈中.简单的说:"该修改的反向动作添加到undo栈中". 9.1是在作了上面3个修改后的undo
5df7dcafx631d86aafade.jpg 如果这时候用户点Undo,那么第一个invocation将会抛出并调用.person raise设置成0.如果用户再次点Undo,那么personname将会修改回"New Employee"

每一次从Undo栈弹出执行一项时,反向操作将会压入到redo栈中.所以,执行了上面说的两个undo动作后,undoredo栈将会是这样的如图9.2
5df7dcafx631d875bc307.jpeg
undo manager是非常智能的,当用户做编辑动作时,undo invocation将加入到undo栈中,当用undo编辑时,undo invocation将加入到redo . 而当用redo编辑时, undo invocation又加入到undo栈中. 这样操作都是自动完成的. 们的任务仅仅是提供给undo manager需要做反向操作的invocation.

现在假设我们编写一个方法 makeItHotter, 它的反向操作方法 makeItColder. 看看是如何undo
- (void)makeItHotter
{
    temperature = temperature + 10;
    [[undoManager prepareWithInvocationTarget:self] makeItColder];
    [self showTheChangesToTheTemperature];
}

你可能猜到了, prepareWithInvocationTarget: 记录了target [self].并且返回undo manager 它自己. undo manager载了forwardInvocation: invocation-makeItColder: 加入到 undo栈中

所以,们还有实现方法makeItColder
- (void)makeItColder
{
    temperature = temperature - 10;
    [[undoManager prepareWithInvocationTarget:self] makeItHotter];
    [self showTheChangesToTheTemperature];
}
们在undo manager中注册了反向操作. 执行undo,makeItColder将被执行,而它的反向makeItHotter将会添加到redo栈中

每个栈中的invocation会是聚合的. 认的,单一事件[做了一个操作]发生时加入到栈中的所有invocation将会是聚合在一起 [这里要理解什么是invocation, 简单来讲,它就是某个对象的某个方法. 所以当你做某个单一操作时,可能会涉及到多个对象,多个方法. 也就是多个invocation]. 所以,当用户的一个操作改变了多个对象时如果点undo ,那么所有的改变都会一次undo完成.

们也可以来修改菜单 Undo Redo 标题. 比如使用Undo Insert来代替简单的Undo. 可以使用如下代
[undoManager setActionName:@"Insert"];

那么,怎么得到一个undo manager?你可以直接创建. 过注意,NSDocument对象已经有一个自己的undo manager [它也是自己创建的哈]


RaiseMan添加Undo功能

为了使用户可以使用undo功能: undo Add New Employess Delete.以及undo person对象的修改. 们必须给MyDocument添加代

当我计类时, 我会考虑为什么要定义一个成员变量? 一定是下面的4个目的之一
1.
简单的属性: 比如学生的名字. 们一般会是数字或NSString,NSNumber,NSDate,NSData对象
2.
单一关系: 比如一个学生一定会有一个学校和他相关. 这和1较像,只是它的类型是一个复杂对象.单一关系使用指针来实现: 学生对象有一个指向学校对象的指
3.
有序的多元关系: 比如,每个播放列表会有一系列歌曲和它关. 这些歌曲有特定的顺序. 这样的关系一般使用NSMutableArray
4.
无序的多元关系: 比如,每个部门会有一些雇,们可以对雇员按某个方式来排序(比如按照姓氏),过这样的顺序都不是本质上的顺序. 一般使用NSMutalbeSet.

早些时候,们讨论了怎样使用key-vaule coding设置简单属性和单一关系
.setting 或是 getting fido值时, key-value coding使用accessor 方法.样的我们可以为有序的多元关系和无序的多元关系创建accessor方法.

来看看,对象playlist有一个NSMutabelArray变量来存放Song对象.如果你使用key-value coding来操作这个array对象,你将调用mutableArrayVauleForKey: .  得到一个代理对象,这个代理对象表示那个array.
id arrayProxy = [playlist mutableArrayValueForKey:@"songs"];
int songCount = [arrayProxy count];
这个例子中.调用 count方法.代理对象将先看playlist对象有没有实 countOfSongs方法.如果有,那么就会调用该方法并返回结果.如果没有, 那么会调用保存songarraycount 方法如9.3. 注意.方法countOfSongs的命名不仅仅是因为编码习惯: key-vaule coding机制使用这样的名字查找
5df7dcafx631d88256cdf.jpg
下面是几个例子
id arrayProxy = [playlist mutableArrayValueForKey:@"songs"];

int x = [arrayProxy count]; // is the same as
int x = [playlist countOfSongs]; // if countOfSongs exists

id y = [arrayProxy objectAtIndex:5] // is the same as
id y = [playlist objectInSongsAtIndex:5]; // if the method exists

[arrayProxy insertObject:p atIndex:4] // is the same as
[playlist insertObject:p inSongsAtIndex:4]; // if the method exists

[arrayProxy removeObjectAtIndex:3] // is the same as
[playlist removeObjectFromSongsAtIndex:3] // if the method exists

对于无序多元关系也是一样的如9.4

5df7dcafx631d88c196f2.jpg
id setProxy = [teacher mutableSetValueForKey:@"students"];

int x = [setProxy count]; // is the same as
int x = [teacher countOfStudents]; // if countOfStudents exists

[setProxy addObject:newStudent]; // is the same as
[teacher addStudentsObject:newStudent]; // if the method exists

[setProxy removeObject:expelledStudent]; // is the same as
[teacher removeStudentsObject:expelledStudent]; // if the method exists

为我们绑定了array controllercontentArrayMydocument对象的employees. 所以array controller将会使用key-vaule coding来添加和删除person对象. 们可以使用这个机制来实现当添加person对象时添加unod invocationundo. MyDocument,m添加如下方法
- (void)insertObject:(Person *)p inEmployeesAtIndex:(int)index
{
    NSLog(@"adding %@ to %@", p, employees);
    // Add the inverse of this operation to the undo stack
    NSUndoManager *undo = [self undoManager];
    [[undo prepareWithInvocationTarget:self]
                          removeObjectFromEmployeesAtIndex:index];
    if (![undo isUndoing]) {
        [undo setActionName:@"Insert Person"];
    }
    // Add the Person to the array
    [employees insertObject:p atIndex:index];
}

- (void)removeObjectFromEmployeesAtIndex:(int)index
{
    Person *p = [employees objectAtIndex:index];
    NSLog(@"removing %@ from %@", p, employees);
    // Add the inverse of this operation to the undo stack
    NSUndoManager *undo = [self undoManager];
    [[undo prepareWithInvocationTarget:self] insertObject:p
                                       inEmployeesAtIndex:index];
    if (![undo isUndoing]) {
        [undo setActionName:@"Delete Person"];
    }

    [employees removeObjectAtIndex:index];
}

NSArrayController添加或是删除Person对象时,这些方法会自动调用:例如, Create New Delete 钮发送insert: remove: 消息的时候

MyDocument.h中声明
- (void)removeObjectFromEmployeesAtIndex:(int)index;
- (void)insertObject:(Person *)p inEmployeesAtIndex:(int)index;
由于使用了Person.所以我们需要告知编译器. MyDocument.h中添加
#import <Cocoa/Cocoa.h>
@class Person;
,MyDocument.m导入Person.h
#import "Person.h"

好了,们已经可以undo添加和删除了. 对于undo 编辑会有点复杂. 在搞定它前,编译运行我们的程序.试试undo功能. 注意,redo功能也是可用的

Key-Vaule Observing
在第7,们讨论了key-vaule coding. 忆一下,key-vaule coding是一种通过变量名字读取和修改变量值的方法. key-vaule  observing是当这些改变发生时我们能得到通知.

为了实现undo 编辑,们需要让document对象能够得到改变了Person对象expectedRaisepersonName的通知. NSObject的一个方法可以用来注册这样的通知
- (void)addObserver:(NSObject *)observer
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(void *)context;

对象observer为要通知的对象, keyPath标识激活通知的改变. options义了通知包含的内容选,例如,是否包含改变前的值,是否包含改变后的值. context是一个随着通知一起发送的对象,可以包含任何信息.一般NULL.

当一个改变发生, observer对象将收到下面的消息
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context;
observer会知道那个对象的那个key path变了. change是一个dictionary,其中的内容会依据在注册是option指定的. 可能包含改变前的值和()变后的值. context针就是注册是的context,通常情况下,忽略它.

Undo编辑

第一步是将document对象注册观察它自己的person对象改变.MyDocument.m中添加如下方法
- (void)startObservingPerson:(Person *)person
{
    [person addObserver:self
             forKeyPath:@"personName"
                options:NSKeyValueObservingOptionOld
                context:NULL];

    [person addObserver:self
             forKeyPath:@"expectedRaise"
                options:NSKeyValueObservingOptionOld
                context:NULL];
}

- (void)stopObservingPerson:(Person *)person
{
    [person removeObserver:self forKeyPath:@"personName"];
    [person removeObserver:self forKeyPath:@"expectedRaise"];
}

在添加或删除Person对象是调用上面的方法
- (void)insertObject:(Person *)p inEmployeesAtIndex:(int)index
{
    // Add the inverse of this operation to the undo stack
    NSUndoManager *undo = [self undoManager];
    [[undo prepareWithInvocationTarget:self]
         removeObjectFromEmployeesAtIndex:index];
    if (![undo isUndoing]) {
        [undo setActionName:@"Insert Person"];
    }

    // Add the Person to the array
    [self startObservingPerson:p];
    [employees insertObject:p atIndex:index];
}

- (void)removeObjectFromEmployeesAtIndex:(int)index
{
    Person *p = [employees objectAtIndex:index];
    // Add the inverse of this operation to the undo stack
    NSUndoManager *undo = [self undoManager];
    [[undo prepareWithInvocationTarget:self] insertObject:p
                                       inEmployeesAtIndex:index];
    if (![undo isUndoing]) {
        [undo setActionName:@"Delete Person"];
    }
    [self stopObservingPerson:p];
    [employees removeObjectAtIndex:index];
}

- (void)setEmployees:(NSMutableArray *)a
{
    if (a == employees)
        return;

    for (Person *person in employees) {
        [self stopObservingPerson:person];
    }

    [a retain];
    [employees release];
    employees = a;
    for (Person *person in employees) {
        [self startObservingPerson:person];
    }
}

现编辑修改方法
- (void)changeKeyPath:(NSString *)keyPath
             ofObject:(id)obj
              toValue:(id)newValue
{
    // setValue:forKeyPath: will cause the key-value observing method
    // to be called, which takes care of the undo stuff
    [obj setValue:newValue forKeyPath:keyPath];
}

现当Person 对象编辑通知响应方法,
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    NSUndoManager *undo = [self undoManager];
    id oldValue = [change objectForKey:NSKeyValueChangeOldKey];

    // NSNull objects are used to represent nil in a dictionary
    if (oldValue == [NSNull null]) {
        oldValue = nil;
    }
    NSLog(@"oldValue = %@", oldValue);
    [[undo prepareWithInvocationTarget:self] changeKeyPath:keyPath
                                                  ofObject:object
                                                   toValue:oldValue];
    [undo setActionName:@"Edit"];
}

好了,现在编译运行程序, undo redo功能完全可以工作了.

注意到了? 一旦我们修改了document, 窗口标题栏上的红色关闭按钮会出现一个黑点来提示我们,这些改变没有被保存. 在下一个章,们来学习把它们保存为文件

添加后里面编辑
们的程序看上去运行的很好,过有些用户可能会抱怨"当我插入一条记录后,为什么我必须双击才能开始编辑?很明显的我一定会修改新增person的名字啊."

这会有些复,我打算提供所需的代码片段,首先,MyDocument.h中添加一个acton和两个成员变量
@interface MyDocument : NSDocument
{
    NSMutableArray *employees;
    IBOutlet NSTableView *tableView;
    IBOutlet NSArrayController *employeeController;
}
- (IBAction)createEmployee:(id)sender;
保存文件,(们记住一定要保存.h文件.这样新加的actionoutlet才能在Interface Builder中找到)Interface BuilderControl-drag Add New Employee钮到File's Owner(MyDocument对象). 设置actioncreateEmployee: 9.5
5df7dcafx631d8af27676.jpg

Control-click file's Owner,设置好outlet tableView employeeController9.6
5df7dcafx631d8b991d3f.jpg
MyDocument.m中添加 createEmployee:方法
- (IBAction)createEmployee:(id)sender
{
    NSWindow *w = [tableView window];

    // Try to end any editing that is taking place
    BOOL editingEnded = [w makeFirstResponder:w];
    if (!editingEnded) {
        NSLog(@"Unable to end editing");
        return;
    }
    NSUndoManager *undo = [self undoManager];

    // Has an edit occurred already in this event?
    if ([undo groupingLevel]) {
        // Close the last group
        [undo endUndoGrouping];
        // Open a new group
        [undo beginUndoGrouping];
    }
    // Create the object
    Person *p = [employeeController newObject];

    // Add it to the content array of 'employeeController'
    [employeeController addObject:p];
    [p release];
    // Re-sort (in case the user has sorted a column)
    [employeeController rearrangeObjects];

    // Get the sorted array
    NSArray *a = [employeeController arrangedObjects];

    // Find the object just added
    int row = [a indexOfObjectIdenticalTo:p];
    NSLog(@"starting edit of %@ in row %d", p, row);

    // Begin the edit in the first column
    [tableView editColumn:0
                      row:row
                withEvent:nil
                   select:YES];
}
不能期望你能理解没一行代.试着浏览这些方法,立即它们的基本原理. 编译运行程序吧.


思考: WindowsUndo Manager
可以把view编辑动作加入到undo manager.例如, NSTextView,可以把文字输入的动作加入到undo manager.使用Interface Builder来激活 9.7

那么text view是怎么知道使用哪一个undo manager?首先,它会delegate. NSTextViewdelegate可以现这个方法
- (NSUndoManager *)undoManagerForTextView:(NSTextView *)tv;
接下来,它会问他的window. NSWindow有一个方法
- (NSUndoManager *)undoManager;
windowdelegate可以现一个方法来说明是否window可以提供undo manager
- (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)window;

Undo/redo 项反应了当前key windowundo manager(key window也就是大家说的active window. Cocoa 发者叫它key 是因为用户的键盘输入事件由它接受)

 

你可能感兴趣的:(cocoa,manager,insert,interface,Dictionary,accessor)