3

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

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

编辑:
澄清一下,再考虑一下,我想我真正想要的是一种操作撤消/重做堆栈的方法,这样我就可以避免 Rob Napier 在他的回答中提到的“不一致状态”。

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

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

4

2 回答 2

1

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

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

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

- (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];
}

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

于 2009-11-02T14:27:32.043 回答
1

我有两个答案给你。第一种是处理这种情况的常用方法:

当您点击取消按钮时调用 undo 并让 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 中的控制器上)。我保留了对原始文件的引用,以便在完成后可以将事情恢复正常。

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

如果操作成功,那就有点棘手了。此时需要注册UndoWithTarget: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 文档中找到这一点,这只是我的看法。

于 2009-12-18T03:20:28.640 回答