У меня есть основное приложение Какао, использующее NSUndoManager для поддержки отмены / повтора. Название моей модели данных можно обновить, отредактировав NSTextField
. Из коробки NSTextField поддерживает объединение изменений в одно действие отмены «Отменить ввод», так что когда пользователь нажимает Cmd-Z, текст полностью возвращается вместо последней буквы.
Мне бы хотелосьиметь единственное действие отмены «Отменить изменение названия модели», которое отменяет множественные изменения в названии моей модели (сделано в NSTextField). Я попытался сгруппировать изменения самостоятельно, но мое приложение вылетает (см. Ниже).
Я не могу использовать поведение отмены по умолчанию NSTextField, потому что мне нужно обновить мою модель, и к тому времени текстовое поле может исчезнутьпользователь пытается отменить действие (текстовое поле находится во всплывающем окне).
NSUndoManager по умолчанию группирует изменения, которые происходят в течение одного цикла цикла выполнения, но также позволяет отключить это поведение и создать пользовательский "отменить группы ". Поэтому я попробовал следующее:
- Установить
undoManager.groupsByEvent = false
- В
controlTextDidBeginEditing()
, начать новую группу отмены - В
controlTextDidChange()
, зарегистрировать действие отмены - В
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. Когда я набираю первую букву, она начинается и сразу же завершает отмену группы. Он не создает никаких других групп отмены для последующих изменений после первого изменения.
Для воспроизведения:
- Создайте новый проект Какао и вставьте код для делегата приложения (см. Ниже)
- Введите "a" + "b"+ "c" + Cmd-Z
Ожидается:
- Значение текстового поля должно быть установлено в пустую строку
Фактически:
В качестве альтернативы:
- Введите "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
. Он создает новую группу отмены, когда я набираю первую букву, закрывает группу и не создает какие-либо дополнительные группы отмены для следующих букв, которые я печатаю. Очень странно ...