如何取消正在进行的撤消/重做操作?

时间:2009-10-28 01:31:59

标签: objective-c cocoa undo

我几个月来一直在修补Cocoa,我正在尝试添加撤消/重做支持到我正在编写的严格用于学习目的的Cocoa应用程序,可让您调整iTunes轨道元数据。感谢NSUndoManager的prepareWithInvocationTarget:方法,我已经掌握了基础知识 - 您可以撤消/重做对所选曲目的播放计数和上次播放日期的更改。 (我正在使用appscript-objc获取/设置iTunes曲目数据。)

然而,由于更新大量iTunes曲目可能需要一些时间,我想让用户在正在进行时取消撤消/重做操作,但我没有看到明显的机制使用NSUndoManager完成此任务。我该怎么做呢?

修改
为了澄清,考虑到这一点,我想我真正想要的是一种操纵撤销/重做堆栈的方法,这样我就可以避免Rob Napier在他的回答中提到的“不一致状态”。

因此,为了响应撤消操作而未进行任何更改(例如,在调用撤消之前,用户已打开iTunes的“首选项”窗口,阻止Apple事件),撤消操作可以保留在顶部undo stack,重做堆栈保持不变。如果操作在中途被取消或失败,那么我想在重做堆栈上推送一个操作,该操作可以反转经历的更改(如果有的话),并且在撤消堆栈的顶部有一个应用所做更改的操作。成功。我认为有效地拆除撤销和重做堆栈之间的操作可能会引起用户混淆,但它似乎是解决问题最宽容的方式。

我怀疑这个问题的答案可能是“编写你自己该死的撤销管理器”,“你的撤销操作不应该失败”或“你不必要地过度复杂化”,但我很好奇。 :)

2 个答案:

答案 0 :(得分:1)

这是您的代码的问题,而不是NSUndoManager。无论您请求NSUndoManager呼叫的方法是什么,都需要具有中断功能。这与在最初执行时取消操作没有什么不同(事实上,只要可能,它应该是相同的代码)。无论是否有线程,有两种常见的方法可以实现这一点。

在线程情况下,您在后台线程上运行撤消操作,并在每个循环中检查BOOL,例如self.shouldContinue。如果它被设置为假(通常由其他一些线程),那么你就停止了。

在没有线程的情况下实现此目的的类似方法是这样的:

- (void)doOperation
{
    if ([self.thingsToDo count] == 0 || ! self.shouldContinue)
    {
        return;
    }

    id thing = [self.thingsToDo lastObject];
    [self.thingsToDo removeLastObject];

    // Do something with thing

    [self performSelector:@selector(doOperation) withObject: nil afterDelay:0];
}

这里的一个主要问题是,此取消将使您的撤消堆栈处于不确定状态。由你来处理。同样,这与创建撤消项目时的初始情况没有什么不同。但是,如果处理中断,那么在撤消期间应该如何处理中断。它们通常不是不同的行为。

答案 1 :(得分:1)

我有两个答案。第一种是处理这个问题的常用方法:

当您点击取消按钮时调用撤消,让NSUndoManager回滚所有更改。

请参阅: http://www.cimgf.com/2008/04/30/cocoa-tutorial-wiring-undo-management-into-core-data/ http://www.mac-developer-network.com/columns/coredata/coredatafeb09/ 例如这种方法。他们正在讨论表格,但它应适用于任何可取消的内容。

此方法的问题在于它会留下不一致的重做堆栈。你可以通过调用removeAllActionsWithTarget从NSUndoManager驱逐目标来解决这个问题:

第二种解决方案要复杂得多,但我实际上使用它并且它有效。我在Java中使用移植版本,所以如果示例不在objective-c中,请原谅我,但我会尝试将这个概念贯穿其中。

我严格为操作创建一个新的撤消管理器,并将其设置为“活动”(我认为这意味着在Cocoa中将其设置在控制器上)。我保留了对原始参考的引用,以便在完成后我可以将事情恢复正常。

如果他们点击取消按钮,您可以在主动撤消管理器上调用撤消,释放它并将原始撤消管理器设置回原来的位置。除了最初的那些之外,原始文件不会有任何撤销或重做操作。

如果操作成功,则有点棘手。此时你需要registerUndoWithTarget:selector:object:。这是一个需要发生什么的伪代码:

invoke(boolean undo) {
    oldUndoManager = currentUndoManager
    setCurrentUndoManager(temporaryUndoManager)

    if (undo)
        temporaryUndoManager.undo()
        oldUndoManager.registerUndo(temporaryUndoManager,
                "invoke", false)
    else
        temporaryUndoManager.redo()
        oldUndoManager.registerUndo(temporaryUndoManager,
                "invoke", true)

    setCurrentUndoManager(oldUndoManager)
}

这将允许您的原始(旧)撤消管理器基本上在新(临时)上调用撤消/重做,并在响应中设置相应的撤消/重做。这比第一个复杂得多,但确实帮助我做了我想做的事。

我的理念是,只有完成的操作应该转到撤消管理器。取消的操作是一种从不存在的所有实际目的的操作。你不会在我知道的任何Apple文档中找到它,只是我的意见。