1

在一个看似微不足道的设置中,我遇到了一个可选的NSTextField和一个NSColorPanel之间不受欢迎的相互作用,我无法摆脱它,这让我抓狂。

这是设置:在一个窗口中,我有一个可选择的多行标签(事实上是一个NSTextField)和一个NSColorWell

Color Well允许用户在 GUI 中为几何对象着色;它与文本无关。当然,单击颜色井会激活它,即调出共享的NSColorPanel并将颜色井连接到它。

文本字段完全独立于 GUI 中的彩色对象,并向用户呈现数据。它是只读的,即不可编辑。由于数据是按列组织的,因此我使用选项卡进行文本格式设置和NSTextFieldsetAttributedStringValue:的方法来显示数据。

乍一看,在这样一个微不足道的设置中,一切正常。

但问题来了:我希望用户能够复制文本字段中的数据以在其他地方处理它。因此,NSTextField必须是selectable。将其设置为selectable问题开始的地方:

当用户单击可选文本字段以选择文本时,窗口的字段编辑器将接管,因此,属性文本的所有选项卡设置都丢失并且文本混合在一起。防止这种情况的常用方法是将NSTextFieldallowsEditingTextAttributes的属性设置为. 如果我这样做,则在用户选择文本时会保留选项卡格式。但是现在NSColorPanel(如果可见)无意中切换到文本颜色(始终为黑色),如果颜色池处于活动状态(连接到NSColorPanel),它将保持活动状态,从而将所有几何 GUI 对象的颜色更改为黑色. 哎哟!YES

我发现无法将NSTextFieldselectable的andallowsEditingTextAttributes属性设置为但仍然阻止它与NSColorPanel通信。YES

明显的替代方法是保留所选文本的选项卡格式,即使allowsEditingTextAttributes设置为NO(这将根据需要断开颜色面板与文本字段的连接)。但是我用这种方法也没有成功,虽然我不太明白为什么:

我的想法是将所需的选项卡设置为defaultParagraphStyle文本字段的字段编辑器。因此,我设置了一个自定义字段编辑器:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
    {
        NSArray *myTabs = @[
            [[NSTextTab alloc] initWithType:NSRightTabStopType location:100],
            [[NSTextTab alloc] initWithType:NSRightTabStopType location:200],
            [[NSTextTab alloc] initWithType:NSRightTabStopType location:300]
        ];
        NSMutableParagraphStyle *myParagraphStyle = [[NSMutableParagraphStyle defaultParagraphStyle] mutableCopy];
        [myParagraphStyle setTabStops:myTabs];

        myFieldEditor = [NSTextView new]; // myFieldEditor is an instance variable
        [myFieldEditor setDefaultParagraphStyle:myParagraphStyle];

        [window setDelegate:self];
        [window fieldEditor:YES forObject:myTextField];
    }

windowWillReturnFieldEditor:toObject:并为委托方法中的文本字段激活它:

- (id)windowWillReturnFieldEditor:(NSWindow *)sender toObject:(id)client
    {
        if (client == myTextField) return myFieldEditor;
        return nil;
    }

我什至通过子类化我的文本字段的NSTextFieldCell并记录传播的字段编辑器来确保确实使用了我的自定义字段编辑器:

@implementation myTextFieldCell

- (NSText *)setUpFieldEditorAttributes:(NSText *)textObj
    {
        NSTextView *newTextObj = (NSTextView*)[super setUpFieldEditorAttributes:textObj];
        NSLog(@"STYLE: %@", [newTextObj defaultParagraphStyle]);
        return newTextObj;
    }

@end

现在,当我在文本字段中选择文本时,我得到以下日志输出:

2017-11-02 11:51:07.432 Demo[94807:303] STYLE: Alignment 4, LineSpacing 0, ParagraphSpacing 0, ParagraphSpacingBefore 0, HeadIndent 0, TailIndent 0, FirstLineHeadIndent 0, LineHeight 0/0, LineHeightMultiple 0, LineBreakMode 0, Tabs (
    100R,
    200R,
    300R
), DefaultTabInterval 0, Blocks (null), Lists (null), BaseWritingDirection -1, HyphenationFactor 0, TighteningFactor 0.05, HeaderLevel 0

这正是预期的结果。

但是,一旦我选择文本,选项卡格式就会在文本字段中消失我不知道为什么这不起作用。

所以无论哪种方式,我都被困住了。如果我将NSTextFieldallowsEditingTextAttributes的属性设置为,则在选择文本时会保留选项卡格式,但我在 GUI 中的彩色对象会无意中变为黑色。如果我将该属性设置为,则颜色面板的行为应如此,但一旦我选择文本,选项卡格式就会丢失。YESallowsEditingTextAttributesNO

这是一个非常不幸的案例,Cocoa 试图变得太聪明,从而使完全微不足道的设置成为一个大问题。

有什么想法吗?

4

1 回答 1

2

好的,所以我最终得到了@Willeke(谢谢!!)在他对我的问题的评论中提出的建议:使用NSTextView而不是NSTextField来实现我的Multi-Line Label

我将首先总结为什么用NSTextField做我想做的事情似乎是不可能的,然后用NSTextView做解决方案。

为什么 NSTextField 不起作用

如上所述,我对解决方案的想法是为NSTextField自定义字段编辑器,设置我需要的制表位,这样我就不需要将NSTextFieldallowsEditingTextAttributes属性设置为YES(这会无意中将文本字段耦合到颜色面板)。我希望,当我在文本字段中选择文本并因此激活字段编辑器时,这将保留我的属性字符串段落样式的制表位。

广泛的测试表明,由于以下几个原因,这不起作用:

  1. 正如@Willeke 指出的那样,将NSTextViewusesFontPanel的属性设置为也会中断文本视图与颜色面板的连接(根据需要)。但是,这不适用于作为NSTextField的字段编辑器的NSTextView,因为在这种情况下,此设置总是被NSTextField的属性覆盖:如果是,字体和颜色面板是耦合的,而不管,如果是,字体和颜色面板将被解耦,而不管.NOallowsEditingTextAttributesallowsEditingTextAttributesYESusesFontPanelNOusesFontPanel
  2. 无论如何,使用自定义字段编辑器的制表位而不是使用我的属性字符串段落样式的制表位(这将需要allowsEditingTextAttributesYES)的想法是行不通的,因为字段编辑器的制表位设置显然总是NSTextField完全忽略,无论allowsEditingTextAttributes属性的值如何。NSTextField 总是使用均匀间隔的默认制表位。

从激烈的谷歌搜索来看,另一个变体 ——设置但allowsEditingTextAttributesYES某种方式修改NSColorPanel以不连接到NSTextField——如果不重复使用NSColorPanel的私有方法是不可能实现的。

如何使用 NSTextView 实现解决方案

虽然为了获得文本字段的功能而实例化嵌入在剪辑视图和滚动视图中的完整NSTextView似乎有点矫枉过正,但最终它是最简单(甚至是唯一可能)的解决方案。

要使滚动视图消失,您必须在 IB 中取消选中NSScrollView属性检查器中的所有内容,尤其是Show Vertical Scroller。将绘制背景和边框类型设置为您想要的外观类型;如果你想模仿多行标签(就像我做的那样),取消选中绘制背景并选择不可见的边框类型。在嵌入式NSTextView的属性检查器中,也取消选中除Selectable之外的所有属性,特别是Uses Font Panel

确保NSTextView的大小足以容纳完整的内容字符串,以避免无意的滚动效果并固定文本位置。如果您的内容字符串以换行符结尾,则您需要足够的空间在其下方放置一个空行。如果您没有取消选中Draw Background并且这看起来不像您想要的那样,请不要绘制NSScrollViewNSTextView的背景,为NSScrollView选择不可见边框,然后在它们下方放置一个具有所需大小和外观的NSBox .

您现在可以使用以下方式设置属性内容字符串:

[[myTextView textStorage] setAttributedString:myAttributedString];

请注意,这确实有效,尽管NSTextVieweditable的属性设置为,因为您正在修改NSTextStorage,而不是NSTextView本身。NO

但不幸的是,我们还没有完成。

当您使用NSTextField像通常在Label中那样显示只读数据时,通常您不希望文本字段成为键视图循环的一部分(通过按 Tab 键在控件中循环) . 为此,您只需将NSTextFieldrefusesFirstResponder的属性设置为. 但是NSTextView不继承自NSControl,因此没有这个属性。所以最后,我们必须继承 NSTextView来添加属性。YESrefusesFirstResponder

实现覆盖becomeFirstResponder并如下所示:

- (BOOL)becomeFirstResponder
    {
        if (!_refusesFirstResponder) return [super becomeFirstResponder];

        NSEvent *event = [NSApp currentEvent];
        if ([event type] == NSLeftMouseDown || [event type] == NSRightMouseDown) return [super becomeFirstResponder];

        NSView *validKeyView = ([event modifierFlags] & NSShiftKeyMask)? [[[self previousValidKeyView] previousValidKeyView] previousValidKeyView] : [self nextValidKeyView];
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{[[self window] makeFirstResponder:validKeyView];}];
        return NO;
    }

如果refusesFirstResponderNO,我们简单地返回super的实现。

如果是YES,我们检查NSTextView是否会因为鼠标点击而成为第一响应者。如果是这样,我们也简单地返回super的实现,从而允许用鼠标选择文本。

除此之外,我们将第一响应者请求转发到下一个或上一个键视图(取决于是否按下了Shift键)并返回NO,拒绝成为第一响应者。确定前一个键视图有点棘手,因为最接近的前一个键视图是我们不想要或不需要但必须使用的嵌入NSClipView,因为 Interface Builder 不提供“纯” NSTextView。然后是嵌入的NSScrollView,然后才是我们真正想要的前一个键视图。

此外,由于我们处于确定第一响应者的过程中,我们不能简单地调用makeFirstResponder:,而必须将其推迟到运行循环的下一次迭代。

现在我们已经实现了refusesFirstResponder,我们仍然必须模仿NSTextField的行为,以在它失去第一响应者状态时关闭任何文本选择。我们可以在NSText委托方法中做到这一点。假设我们不需要其他委托功能,我们可以让我们的子类成为自己的委托并添加这个委托方法:

- (void)textDidEndEditing:(NSNotification*)notification
    {
        [[notification object] setSelectedRange:NSMakeRange(UINT64_MAX, 0)];
    }

最后,如果我们必须子类化,无论如何,我们还不如添加一个setAttributedString:方便的方法。

所以我们最终会是这样的:

标题:

#import <Cocoa/Cocoa.h>

IB_DESIGNABLE

@interface MyTextFieldLikeTextView : NSTextView <NSTextViewDelegate>

@property IBInspectable BOOL    refusesFirstResponder;

- (void)setAttributedString:(NSAttributedString*)attributedString;

@end

执行:

#import "MyTextFieldLikeTextView.h"

@implementation MyTextFieldLikeTextView

- (void)awakeFromNib
    {
        [self setDelegate:self];
    }

- (BOOL)becomeFirstResponder
    {
        if (!_refusesFirstResponder) return [super becomeFirstResponder];

        NSEvent *event = [NSApp currentEvent];
        if ([event type] == NSLeftMouseDown || [event type] == NSRightMouseDown) return [super becomeFirstResponder];

        NSView *validKeyView = ([event modifierFlags] & NSShiftKeyMask)? [[[self previousValidKeyView] previousValidKeyView] previousValidKeyView] : [self nextValidKeyView];

        [[NSOperationQueue mainQueue] addOperationWithBlock:^{[[self window] makeFirstResponder:validKeyView];}];
        return NO;
    }

- (void)textDidEndEditing:(NSNotification*)notification
    {
        [[notification object] setSelectedRange:NSMakeRange(UINT64_MAX, 0)];
    }

- (void)setAttributedString:(NSAttributedString*)attributedString
    {
        [[self textStorage] setAttributedString:attributedString];
    }

@end

仍然需要付出很多努力,因为 Cocoa 试图超越我们并坚持将NSColorPanel连接到每个允许属性文本的NSTextField ......

于 2017-11-04T03:31:41.090 回答