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

3
我已经研究了几个月的Cocoa,并试图为我正在编写用于学习目的的Cocoa应用程序添加撤销/重做支持,该应用程序允许您调整iTunes曲目元数据。由于NSUndoManager的prepareWithInvocationTarget:方法,我已经掌握了基本操作——例如,您可以撤消/重做选定曲目的播放计数和上次播放日期的更改。(我使用appscript-objc来获取/设置iTunes曲目数据。)
但是,由于更新大量iTunes曲目可能需要一些时间,因此我希望在撤销/重做操作进行时给用户取消操作的能力,但我没有看到用NSUndoManager实现这一点的明显机制。我该如何解决?
编辑: 为了澄清,经过思考,我认为我真正需要的是一种操纵撤销/重做堆栈的方法,以避免Rob Napier在他的答案中提到的“不一致状态”。
因此,对于一个未进行任何更改的失败的撤销操作(例如,在调用撤销之前,用户已经打开了iTunes的首选项窗口,这会阻止Apple事件),撤销操作将保留在撤销堆栈的顶部,而重做堆栈将保持不变。如果操作被取消或在进行过程中失败,则我希望在重做堆栈上推送一个反转已经完成的更改(如果有的话)并在撤销堆栈的顶部放置一个应用未成功的更改的操作。我想,将操作有效地分割在撤销和重做堆栈之间可能会引起用户混淆,但这似乎是处理此问题的最宽容方式。
我猜想这个问题的答案可能是“自己写Undo Manager”、“您的Undo操作不应该失败”或“您在不必要地过度复杂化此过程”,但我很好奇。 :)
2个回答

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中将其设置为您的控制器)。我保留对原始管理器的引用,以便在完成后将事物恢复到正常状态。

如果用户点击取消按钮,则可以在活动撤销管理器上调用undo,然后释放它并将原始撤销管理器恢复到先前的位置。原始管理器不会有任何未决的撤销或重做操作,除了最初存在的那些操作。

如果该操作成功,则需要进行一些棘手的处理。此时,您需要使用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)
}

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

我的理念是只有完成的操作才应该进入撤销管理器。 取消的操作是一种实际上从未存在过的操作。 我不知道任何苹果文档中有这样的说法,这只是我的个人意见。


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

一个主要的问题是,这个取消操作会让您的撤销堆栈处于不确定状态。你需要自己处理这个问题。这与最初创建撤销项目时没有什么不同。处理中断的方式也应该与处理撤消期间的中断的方式相同。它们通常不是不同类型的操作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接