pjv*_*pjv 15 objective-c nstextview nsundomanager
我在cocoa中编写了一个特殊用途的文本编辑器,它可以执行自动文本替换,内联文本完成(ala Xcode)等操作.
我需要能够以编程方式操纵NSTextView
的NSTextStorage
响应于1)用户打字,2)用户粘贴,3)用户滴文本.
我尝试了两种不同的通用方法,它们都导致了NSTextView
本机撤销管理器以不同的方式失去同步.在每种情况下,我只使用NSTextView
委托方法.我一直试图避免继承NSTextview
或NSTextStorage
(尽管我会在必要时继承).
我想第一种方法是从内做操作textView
delegate
的textDidChange
方法.从该方法中,我分析了已经更改的内容textView
,然后调用了一个通用方法,用于修改包含textStorage中的更改的文本,并调用shouldChangeTextInRange:
和didChangeText:
.一些程序化更改允许清除撤消,但有些没有.
第二个(也许更加直观,因为它使修改文本之前居然出现在textView
)的方法,我试图从内做操作delegate
的shouldChangeTextInRange:
方法,再次使用该包装的变化在存储有相同的通用存储修改方法打电话给shouldChangeTextInRange:
和didChangeText:
.由于这些更改最初是从内部触发的shouldChangeTextInRange:
,因此我设置了一个标志,告诉内部调用shouldChangeTextInRange:
被忽略,以免输入递归黑洞.同样,一些程序化更改允许清除撤消,但有些没有(虽然这次不同,并以不同的方式).
有了这样的背景,我的问题是,是否有人能指出我的一般策略,以编程方式操作存储,以NSTextview
保持撤消管理器清洁和同步?
NSTextview
我应该在哪个委托方法中注意textView中的文本更改(通过键入,粘贴或删除)并对其进行操作NSTextStorage
?或者是通过子类化或者NSTextView
或者唯一的干净方式来做到这一点NSTextStorage
?
zpa*_*ack 12
我最近最近发布了一个类似的问题(感谢OP从那里回到这个问题).
这个问题从来没有真正回答我的满意,但我确实解决了我原来的问题,我认为这也适用于此.
我的解决方案不是用于委托方法,而是用于覆盖NSTextView
.所有的修改都是通过覆盖insertText:
和完成的replaceCharactersInRange:withString:
我的insertText:
覆盖检查要插入的文本,并决定是否插入未修改的文本,或在插入之前进行其他更改.在任何情况下insertText:
都会调用super 来进行实际插入.另外,我insertText:
自己进行撤销分组,基本上是beginUndoGrouping:
在插入文本之前调用,endUndoGrouping:
之后调用.这听起来太简单了,但它对我来说似乎很有用.结果是每个插入的字符都会进行一次撤消操作(例如,有多少"真正的"文本编辑器工作 - 例如,参见TextMate).此外,这使得额外的编程修改与触发它们的操作成为原子.例如,如果用户键入{,并且我以insertText:
编程方式插入},则两者都包含在同一撤消分组中,因此一个撤消撤消两者.我insertText:
看起来像这样:
- (void) insertText:(id)insertString
{
if( insertingText ) {
[super insertText:insertString];
return;
}
// We setup undo for basically every character, except for stuff we insert.
// So, start grouping.
[[self undoManager] beginUndoGrouping];
insertingText = YES;
BOOL insertedText = NO;
NSRange selection = [self selectedRange];
if( selection.length > 0 ) {
insertedText = [self didHandleInsertOfString:insertString withSelection:selection];
}
else {
insertedText = [self didHandleInsertOfString:insertString];
}
if( !insertedText ) {
[super insertText:insertString];
}
insertingText = NO;
// End undo grouping.
[[self undoManager] endUndoGrouping];
}
Run Code Online (Sandbox Code Playgroud)
insertingText
是一个我用来跟踪文本是否被插入的ivar.didHandleInsertOfString:
并且didHandleInsertOfString:withSelection:
是实际上最终执行insertText:
修改内容的调用的函数.它们都很长,但我会在最后加入一个例子.
我只是重写,replaceCharactersInRange:withString:
因为我有时会使用该调用来修改文本,并绕过撤消.但是,您可以通过调用将其挂钩以撤消撤消shouldChangeTextInRange:replacementString:
.所以我的覆盖就是这样.
// We call replaceChractersInRange all over the place, and that does an end-run
// around Undo, unless you first call shouldChangeTextInRange:withString (it does
// the Undo stuff). Rather than sprinkle those all over the place, do it once
// here.
- (void) replaceCharactersInRange:(NSRange)range withString:(NSString*)aString
{
if( [self shouldChangeTextInRange:range replacementString:aString] ) {
[super replaceCharactersInRange:range withString:aString];
}
}
Run Code Online (Sandbox Code Playgroud)
didHandleInsertOfString:
做一个完整的buncha东西,但它的要点是它插入文本(通过insertText:
或replaceCharactersInRange:withString:
),如果它做任何插入返回YES,或如果没有插入则返回NO.它看起来像这样:
- (BOOL) didHandleInsertOfString:(NSString*)string
{
if( [string length] == 0 ) return NO;
unichar character = [string characterAtIndex:0];
if( character == '(' || character == '[' || character == '{' || character == '\"' )
{
// (, [, {, ", ` : insert that, and end character.
unichar startCharacter = character;
unichar endCharacter;
switch( startCharacter ) {
case '(': endCharacter = ')'; break;
case '[': endCharacter = ']'; break;
case '{': endCharacter = '}'; break;
case '\"': endCharacter = '\"'; break;
}
if( character == '\"' ) {
// Double special case for quote. If the character immediately to the right
// of the insertion point is a number, we're done. That way if you type,
// say, 27", it works as you expect.
NSRange selectionRange = [self selectedRange];
if( selectionRange.location > 0 ) {
unichar lastChar = [[self string] characterAtIndex:selectionRange.location - 1];
if( [[NSCharacterSet decimalDigitCharacterSet] characterIsMember:lastChar] ) {
return NO;
}
}
// Special case for quote, if we autoinserted that.
// Type through it and we're done.
if( lastCharacterInserted == '\"' ) {
lastCharacterInserted = 0;
lastCharacterWhichCausedInsertion = 0;
[self moveRight:nil];
return YES;
}
}
NSString* replacementString = [NSString stringWithFormat:@"%c%c", startCharacter, endCharacter];
[self insertText:replacementString];
[self moveLeft:nil];
// Remember the character, so if the user deletes it we remember to also delete the
// one we inserted.
lastCharacterInserted = endCharacter;
lastCharacterWhichCausedInsertion = startCharacter;
if( lastCharacterWhichCausedInsertion == '{' ) {
justInsertedBrace = YES;
}
return YES;
}
// A bunch of other cases here...
return NO;
}
Run Code Online (Sandbox Code Playgroud)
我想指出这个代码没有经过实战测试:我还没有在运送应用程序中使用它(尚未).但它是我打算在今年晚些时候发布的项目中使用的代码的精简版本.到目前为止它似乎运作良好.
为了真正了解它是如何工作的,你可能想要一个示例项目,所以我在github上发布了一个.