Пользовательское объединение NSUndoManager изменений в NSTextField приводит к сбою - PullRequest
2 голосов
/ 16 октября 2019

У меня есть основное приложение Какао, использующее NSUndoManager для поддержки отмены / повтора. Название моей модели данных можно обновить, отредактировав NSTextField. Из коробки NSTextField поддерживает объединение изменений в одно действие отмены «Отменить ввод», так что когда пользователь нажимает Cmd-Z, текст полностью возвращается вместо последней буквы.

Мне бы хотелосьиметь единственное действие отмены «Отменить изменение названия модели», которое отменяет множественные изменения в названии моей модели (сделано в NSTextField). Я попытался сгруппировать изменения самостоятельно, но мое приложение вылетает (см. Ниже).

Я не могу использовать поведение отмены по умолчанию NSTextField, потому что мне нужно обновить мою модель, и к тому времени текстовое поле может исчезнутьпользователь пытается отменить действие (текстовое поле находится во всплывающем окне).

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

  1. Установить undoManager.groupsByEvent = false
  2. В controlTextDidBeginEditing(), начать новую группу отмены
  3. В controlTextDidChange(), зарегистрировать действие отмены
  4. В controlTextDidEndEditing(), конец группы отмены

Это работает, пока я заканчиваю редактирование текстового поля нажатием Enter. Если я наберу «abc» и нажму Cmd-Z для отмены перед завершением редактирования, приложение будет аварийно завершено, поскольку группа отмены не была закрыта:

[General] undo: NSUndoManager 0x60000213b1b0 is in invalid state, undo was called
with too many nested undo groups

Из документов undo() должен закрытьотменить группировку автоматически, если это необходимо. Уровень группировки в моем случае равен 1.

undo () [...] Этот метод также вызывает endUndoGrouping (), если уровень вложенности равен 1

* 1034. *

Однако группа отмен не закрывается функцией отмены (), независимо от того, установлено ли для groupsByEvent значение true или false. Мое приложение всегда вылетает.

Что интересно:

Я наблюдал поведение по умолчанию NSTextField. Когда я набираю первую букву, она начинается и сразу же завершает отмену группы. Он не создает никаких других групп отмены для последующих изменений после первого изменения.

Для воспроизведения:

  1. Создайте новый проект Какао и вставьте код для делегата приложения (см. Ниже)
  2. Введите "a" + "b"+ "c" + Cmd-Z

Ожидается:

  • Значение текстового поля должно быть установлено в пустую строку

Фактически:

  • Сбой

В качестве альтернативы:

  1. Введите "a" + "b" + "c" + Enter + Cmd-Z

Результат:

  • Сказанное выше работает, потому что на этот раз группа отмены заканчивается. Все сгруппированные изменения отменены должным образом.

Проблема заключается в том, что пользователь может нажать Cmd-Z в любое время во время редактирования. Я не могу завершить группу отмены после каждого изменения, или изменения не могут быть отменены все сразу.

undo () не закрытие группы отмены может быть ошибкой, но, тем не менее, группировка изменений и отмена во время ввода должны бытьвозможно, потому что это то, что NSTextField делает из коробки (и это работает).

Источник:

import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, NSTextFieldDelegate {

    @IBOutlet weak var window: NSWindow!

    private var textField:NSTextField!

    private let useCustomUndo = true

    private var myModelTitle = ""

    func applicationDidFinishLaunching(_ aNotification: Notification) {

        //useCustomUndo = false

        observeUndoManger()

        setup()

        NSLog("Window      undo manager: \(Unmanaged.passUnretained(window.undoManager!).toOpaque())")
        NSLog("NSTextField undo manager: \(Unmanaged.passUnretained(textField.undoManager!).toOpaque())")
    }

    func controlTextDidBeginEditing(_ obj: Notification) {
        NSLog("Did begin editing & starting undo group")

        // With or without grouping the app crashes on undo if the group was not ended:
        window.undoManager?.groupsByEvent = true

        window.undoManager?.beginUndoGrouping()
    }

    func controlTextDidEndEditing(_ obj: Notification) {
        NSLog("Did end editing & ending undo group")

        window.undoManager?.endUndoGrouping()
    }

    func controlTextDidChange(_ obj: Notification) {
        NSLog("Text did change")

        setModelTitleWithUndo(title: textField.stringValue)
    }

    private func setModelTitleWithUndo(title:String) {
        NSLog("Current groupingLevel: \(window.undoManager!.groupingLevel)")

        window.undoManager?.registerUndo(withTarget: self, handler: {[oldValue = myModelTitle, weak self] _  in

           guard let self = self, let undoManager = self.window.undoManager else { return }

           NSLog("\(undoManager.isUndoing ? "Undo" : "Redo") from current model    : '\(self.myModelTitle)' to: '\(oldValue)'")
           NSLog(                                      "     from current textfield: '\(self.textField.stringValue)' to: '\(oldValue)'")

            self.setModelTitleWithUndo(title: oldValue)
        })

        window.undoManager?.setActionName("Change Title")

        myModelTitle = title

        if window.undoManager?.isUndoing ?? false || window.undoManager?.isRedoing ?? false
        {
            textField.stringValue = myModelTitle
        }

        NSLog("Model: '\(myModelTitle)'")
    }

    private func observeUndoManger() {
        for i in [(NSNotification.Name.NSUndoManagerCheckpoint       , "<checkpoint>"          ),
                  (NSNotification.Name.NSUndoManagerDidOpenUndoGroup , "<did open undo group>" ),
                  (NSNotification.Name.NSUndoManagerDidCloseUndoGroup, "<did close undo group>"),
                  (NSNotification.Name.NSUndoManagerDidUndoChange    , "<did undo change>"     ),
                  (NSNotification.Name.NSUndoManagerDidRedoChange    , "<did redo change>"     )]
        {
            NotificationCenter.default.addObserver(forName: i.0, object: nil, queue: nil) {n in

                let undoManager = n.object as! UndoManager

                NSLog("\(Unmanaged.passUnretained(undoManager).toOpaque()) \(i.1) grouping level: \(undoManager.groupingLevel), groups by event: \(undoManager.groupsByEvent)")
            }
        }
    }

    private func setup() {
       textField = NSTextField(string: myModelTitle)
       textField.translatesAutoresizingMaskIntoConstraints = false

       window.contentView!.addSubview(textField)

       textField.leadingAnchor.constraint(equalTo: window.contentView!.leadingAnchor).isActive = true
       textField.topAnchor.constraint(equalTo: window.contentView!.topAnchor, constant: 50).isActive = true
       textField.trailingAnchor.constraint(equalTo: window.contentView!.trailingAnchor).isActive = true

       if useCustomUndo
       {
           textField.cell?.allowsUndo = false
           textField.delegate = self
       }
    }
}

ОБНОВЛЕНИЕ:

Я наблюдал поведение NSTextField еще немного. NSTextField группирует , а не в нескольких циклах цикла выполнения. NSTextField группирует по событию и использует groupsByEvent = true. Он создает новую группу отмены, когда я набираю первую букву, закрывает группу и не создает какие-либо дополнительные группы отмены для следующих букв, которые я печатаю. Очень странно ...

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...