Как я могу отменить текущую операцию отмены / возврата? - PullRequest
3 голосов
/ 28 октября 2009

Я работаю с Cocoa уже несколько месяцев и пытаюсь добавить поддержку отмены / повтора в приложение Cocoa, предназначенное исключительно для обучения, которое я пишу, которое позволяет вам настраивать метаданные трека iTunes. Благодаря методу prepareWithInvocationTarget: NSUndoManager у меня есть основы - например, вы можете отменить / повторить изменения в Счетчиках воспроизведения и Дате последнего воспроизведения выбранных треков. (Я использую appscript-objc для получения / установки данных трека iTunes.)

Однако, поскольку обновление большого количества треков iTunes может занять некоторое время, я хотел бы дать пользователю возможность отменить операцию отмены / возврата во время ее выполнения, но я не вижу очевидного механизма чтобы сделать это с NSUndoManager. Как бы я поступил так?

Edit:
Чтобы уточнить, подумав об этом немного больше, я думаю, что на самом деле я ищу способ манипулирования стеками отмены / повтора, чтобы избежать «противоречивого состояния», которое Роб Нейпир упоминает в своем ответе.

Таким образом, в ответ на операцию отмены, которая завершилась неудачно без внесения каких-либо изменений (скажем, перед вызовом отмены пользователь открыл окно настроек iTunes, которое блокирует события Apple), операция отмены могла остаться в верхней части окна. стек отмены, и стек повторения будет оставлен без изменений. Если операция была отменена или потерпела неудачу в среднем потоке, то я бы хотел перенести в стек повторных операций операцию, которая отменяет изменения, которые были пройдены (если они есть), и иметь поверх стека отмены операцию, которая применяет изменения, которые не ' т успеха. Я полагаю, что эффективное разделение операции между стеками отмены и повторения может вызвать путаницу среди пользователей, но, похоже, это самый щадящий способ решения проблемы.

Я подозреваю, что ответ на этот вопрос может быть одним из следующих: «Напишите свой собственный проклятый менеджер отмен», «Ваши операции отмены не должны завершаться ошибкой» или «Вы слишком усложняете это», но мне любопытно. :)

Ответы [ 2 ]

1 голос
/ 18 декабря 2009

У меня есть два ответа для вас. Первый - это обычный способ справиться с этим:

Вызовите отмену, когда вы нажмете кнопку отмены, и пусть NSUndoManager откатит все изменения за вас.

См: http://www.cimgf.com/2008/04/30/cocoa-tutorial-wiring-undo-management-into-core-data/ http://www.mac -developer-network.com / колонки / CoreData / coredatafeb09 / для примеров этого метода. Они обсуждают листы, но это должно применяться ко всему, что можно отменить.

Проблема с этим методом в том, что он оставит несовместимый стек повторного выполнения. Вы можете решить эту проблему, изгнав цель из NSUndoManager с помощью вызова removeAllActionsWithTarget:

Второе решение намного сложнее, но на самом деле я им пользуюсь, и оно работает. Я использую портированную версию в Java, так что простите, если пример не в target-c, но я все равно попытаюсь донести эту концепцию.

Я создаю новый менеджер отмены строго для операции и устанавливаю его так, чтобы он был «активным» (я думаю, что это означает установку его на ваш контроллер в Какао). Я сохраняю ссылку на оригинал, чтобы после завершения работы можно было вернуться к нормальной жизни.

Если они нажимают кнопку отмены, вы можете вызвать отмену в своем активном диспетчере отмены, отпустите его и установите исходный менеджер отмены в прежнее положение. В оригинале не будет никаких действий отмены или повтора, кроме тех, которые были там изначально.

Если операция прошла успешно, то это немного сложно. На данный момент вам нужно зарегистрироваться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, о которой я знаю, только мое мнение.

1 голос
/ 02 ноября 2009

Это проблема для вашего кода, а не для 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];
}

Основной проблемой здесь является то, что эта отмена оставит ваш стек отмены в неопределенном состоянии. Вам решать с этим. Опять же, это ничем не отличается от первоначального случая, когда вы создали элемент отмены. Тем не менее, вы имеете дело с прерываниями во время отмены. Они не являются вообще разными видами действий.

...