可选择的 NSTextField 和 NSColorPanel——如何打破它们不受欢迎的相互作用?

selectable NSTextField and NSColorPanel – how to break their undesired interplay?

在一个看似微不足道的设置中,我遇到了 selectable NSTextFieldNSColorPanel 之间的意外相互作用无法摆脱,这让我抓狂。

设置如下:在一个 window 内,我有一个 select 可用的 多行标签 (事实上是 NSTextField ) 和一个 NSColorWell.

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

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

乍一看,在如此简单的设置中,一切都如您所愿。

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

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

我发现无法将 NSTextFieldselectableallowsEditingTextAttributes 属性设置为 YES 但仍然阻止它与NSColorPanel.

明显的替代方法是保留 selected 文本的标签格式,即使 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

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

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

这正是预期的结果。

但是仍然,一旦我 select 文本,制表符格式就会在文本字段中消失。我不知道为什么这不起作用。

所以无论哪种方式我都被困住了。如果我将 NSTextFieldallowsEditingTextAttributes 属性 设置为 YES,则在 selected 文本时保留制表符格式,但是我的GUI 中的彩色对象无意中变为黑色。如果我将 allowsEditingTextAttributes 属性 设置为 NO,颜色面板会正常运行,但是一旦我 select 文本,标签格式就会丢失。

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

有什么想法吗?

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

我先总结一下为什么用NSTextField做我想做的事似乎不可能,然后用NSTextView解决。

为什么 NSTextField 不起作用

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

大量测试表明这不起作用,原因如下:

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

从密集的谷歌搜索来看,其他变体 – 将 allowsEditingTextAttributes 设置为 YES 但不知何故修改了 NSColorPanel尽管如此,不连接到 NSTextField——如果不重复使用 NSColorPanel.

的私有方法是不可能实现的

如何使用 NSTextView 实现解决方案

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

要使滚动视图消失,您基本上必须取消选中 IB 中 NSScrollViewAttribute 检查器中的所有内容,特别是 显示垂直滚动条 。将 绘制背景 和边框类型设置为您想要的外观;如果你想模仿 multi-line 标签(就像我做的那样),取消选中 Draw Background 并选择不可见的边框类型。在嵌入式 NSTextViewAttribute 检查器中,还取消选中除 Selectable 之外的所有属性,特别是 使用字体面板

确保 NSTextView 的大小足以容纳完整的内容字符串,以避免意外的滚动效果并固定文本位置。如果您的内容字符串以换行符结尾,则您需要足够的 space 以在其下方留出一个空行。如果您 not 取消选中 Draw Background 并且这看起来不是您想要的方式,请不要绘制 NSScrollViewNSTextView, select NSScrollView 的不可见边框然后放一个 NSBox 具有所需的尺寸和外观。

您现在可以设置属性内容字符串:

[[myTextView textStorage] setAttributedString:myAttributedString];

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

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

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

实现覆盖了 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,我们模拟ly return super的实现。

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

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

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

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

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

最后,如果非要子类化的话,反正我们不妨加一个setAttributedString:方便的方法。

所以我们最终得到的是:

Header:

#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 允许属性文本......