Problem adding subview to NSScroller subclass

  • Hi all, I am creating a subclass of NSScroller so that I can add an accessory view. I have worked out the position to add it, and added it as a subview, but it is never drawn.

    I have overridden the rectForPart: method to adjust the scroller size, and that leaves the gap as it should.

    I overrode the drawSelf: method and got it to draw the frame of the accessory view, to make sure it was positioned correctly, and it drew in the right place.

    Any ideas what I might be missing? Has anybody successfully added a subview to an NSScroller?

    Thanks

    Gideon
  • On Jul 7, 2012, at 4:38 AM, Gideon King wrote:
    > Any ideas what I might be missing? Has anybody successfully added a subview to an NSScroller?

    FWIW I did a quick test where all I did was add a subview to one of the NSScrollers in an NSScrollView, and it drew just fine.

    Have you double-checked that the subview is really added? What if you NSLog its frame -- could it be out of bounds? As a sanity check, what if you set the subview's frame to be the NSScroller's bounds?

    --Andy
  • On Jul 7, 2012, at 1:38 AM, Gideon King <gideon...> wrote:

    >
    > I overrode the drawSelf: method and got it to draw the frame of the accessory view, to make sure it was positioned correctly, and it drew in the right place.

    Wait, you told another view to draw from within a separate view's -drawRect:? That's not going to work; the coordinate systems aren't set up right.

    --Kyle Sluder
  • I took that to mean he was drawing the frame of the accessory view with something like NSFrameRect(), not sending a draw message to the accessory view. Come to think of it, this answers one of my questions. Assuming the test code is something like...

        // Sanity-check the frame of the accessory view.
        [[NSColor redColor] set];
        NSFrameRect([accessoryView frame]);

    ...it would appear the accessory view has the right frame. My only remaining question then is to confirm that the accessory view really is a subview of the NSScroller, which is easily checked with a breakpoint in the above code.

    When is the superview-subview relationship established? Is *that* code getting called?

    --Andy

    On Jul 7, 2012, at 4:23 PM, Kyle Sluder wrote:

    > On Jul 7, 2012, at 1:38 AM, Gideon King <gideon...> wrote:
    >
    >>
    >> I overrode the drawSelf: method and got it to draw the frame of the accessory view, to make sure it was positioned correctly, and it drew in the right place.
    >
    > Wait, you told another view to draw from within a separate view's -drawRect:? That's not going to work; the coordinate systems aren't set up right.
    >
    > --Kyle Sluder
  • On 07/07/2012, at 6:38 PM, Gideon King wrote:

    > Has anybody successfully added a subview to an NSScroller?

    Yes, but more recently I took it out again and moved that extra view elsewhere, because on Lion/Mountain Lion, these scroll areas are handled differently and the presence of an extra view is detected and used to revert the scroller to the "legacy" scrollbar design, which is going to look increasingly out of place as apps adopt the new one. This extra special-casing could also be affecting whether and how your view is drawn. That said my code worked on Lion, it just used the legacy scrollbars.

    My subclass of NSScrollView only overrides one method - tile, and adds a property, 'placard' which is the extra view to insert. I believe this code originally was derived from someone else's example I found on the web in about 2003, so I'm not trying to claim it as my own. It does work however.

    - (void) setPlacard:(NSView *)inView
    {
    [inView retain];
    if (nil != placard)
    {
      [placard removeFromSuperview];
      [placard release];
    }
    placard = inView;
    [self addSubview:placard];
    }

    - (void)tile
    {
    [super tile];
    if (placard && [self hasHorizontalScroller])
    {
      NSScroller *horizScroller;
      NSRect horizScrollerFrame, placardFrame;

      horizScroller = [self horizontalScroller];
      horizScrollerFrame = [horizScroller frame];
      placardFrame = [placard frame];

      // Now we'll just adjust the horizontal scroller size and set the placard size and location.

      horizScrollerFrame.size.width -= placardFrame.size.width;
      [horizScroller setFrameSize:horizScrollerFrame.size];

      placardFrame.origin.x = NSMinX(horizScrollerFrame);

      // Move horizontal scroller over to the right of the placard

      horizScrollerFrame.origin.x = NSMaxX(placardFrame);
      [horizScroller setFrameOrigin:horizScrollerFrame.origin];

      // Adjust height of placard
      placardFrame.size.height = horizScrollerFrame.size.height;// + 1.0;
      placardFrame.origin.y = [self bounds].size.height - placardFrame.size.height;

      // Move the placard into place
      [placard setFrame:placardFrame];
    }
    }

    --Graham
  • Yes, you are correct - I was just drawing the frame of the accessory view as a sanity check, and it worked as expected.

    It turns out that the problem is with the 10.7+ overlay scroller drawing. If I return NO from +isCompatibleWithOverlayScrollers then it all works as expected. If I return YES, then my subview is not drawn. The subview is present as a subview of the scroll view and in the right place, but not shown.

    Any ideas?

    Regards

    Gideon

    On 08/07/2012, at 7:08 AM, Andy Lee <aglee...> wrote:

    > I took that to mean he was drawing the frame of the accessory view with something like NSFrameRect(), not sending a draw message to the accessory view. Come to think of it, this answers one of my questions. Assuming the test code is something like...
    >
    > // Sanity-check the frame of the accessory view.
    > [[NSColor redColor] set];
    > NSFrameRect([accessoryView frame]);
    >
    > ...it would appear the accessory view has the right frame. My only remaining question then is to confirm that the accessory view really is a subview of the NSScroller, which is easily checked with a breakpoint in the above code.
    >
    > When is the superview-subview relationship established? Is *that* code getting called?
    >
    > --Andy
    >
    > On Jul 7, 2012, at 4:23 PM, Kyle Sluder wrote:
    >
    >> On Jul 7, 2012, at 1:38 AM, Gideon King <gideon...> wrote:
    >>
    >>>
    >>> I overrode the drawSelf: method and got it to draw the frame of the accessory view, to make sure it was positioned correctly, and it drew in the right place.
    >>
    >> Wait, you told another view to draw from within a separate view's -drawRect:? That's not going to work; the coordinate systems aren't set up right.
    >>
    >> --Kyle Sluder
  • Yes, I was using that type of code before too, but it didn't work with the new scrollbar styles (drawing artifacts on resize, not automatically hiding), which is what prompted me to look at subclassing the scroller itself instead. Unfortunately it still only works with the legacy style scrollers and not overlays as per my previous message.

    Regards

    Gideon

    On 08/07/2012, at 11:04 AM, Graham Cox <graham.cox...> wrote:

    >
    > On 07/07/2012, at 6:38 PM, Gideon King wrote:
    >
    >> Has anybody successfully added a subview to an NSScroller?
    >
    >
    > Yes, but more recently I took it out again and moved that extra view elsewhere, because on Lion/Mountain Lion, these scroll areas are handled differently and the presence of an extra view is detected and used to revert the scroller to the "legacy" scrollbar design, which is going to look increasingly out of place as apps adopt the new one. This extra special-casing could also be affecting whether and how your view is drawn. That said my code worked on Lion, it just used the legacy scrollbars.
    >
    > My subclass of NSScrollView only overrides one method - tile, and adds a property, 'placard' which is the extra view to insert. I believe this code originally was derived from someone else's example I found on the web in about 2003, so I'm not trying to claim it as my own. It does work however.
    >
    >
    > --Graham
    >
    >
  • On 08/07/2012, at 12:13 PM, Gideon King wrote:

    > Yes, I was using that type of code before too, but it didn't work with the new scrollbar styles (drawing artifacts on resize, not automatically hiding), which is what prompted me to look at subclassing the scroller itself instead. Unfortunately it still only works with the legacy style scrollers and not overlays as per my previous message.

    That's right. If you do anything to modify the position of the scrollers, it will revert to the legacy scrollbars. That's the way NSScroller works now. AFAIK, you can't have it both ways - add a placard and have overlay scrollers. That's why I moved my placard elsewhere. That said I never experienced drawing artefacts or other problems, it just used the old scrollbars.

    --Graham
  • From memory I think the drawing problems only happened when I was trying to use overlay scrollers.

    With my own scroller implementation, I wondered what was going on, and I see that the backtrace is totally different when I am using overlay scrollers:
        frame #0: 0x0000000100463304 NovaMind5`-[NMPresentableScroller drawRect:] + 148 at NMPresentableScroller.m:90
        frame #1: 0x00007fff838870b2 AppKit`-[NSSurface displayIfNeeded] + 266
        frame #2: 0x00007fff839a9d0d AppKit`-[NSView _recursiveSyncAndDisplayViewBackingSurfacesIfNeeded:] + 212
        frame #3: 0x00007fff87e2f0b6 CoreFoundation`__NSArrayEnumerate + 582
        frame #4: 0x00007fff839a9df6 AppKit`-[NSView _recursiveSyncAndDisplayViewBackingSurfacesIfNeeded:] + 445
        frame #5: 0x00007fff87e2f0b6 CoreFoundation`__NSArrayEnumerate + 582
        frame #6: 0x00007fff839a9df6 AppKit`-[NSView _recursiveSyncAndDisplayViewBackingSurfacesIfNeeded:] + 445
        frame #7: 0x00007fff87e2f0b6 CoreFoundation`__NSArrayEnumerate + 582
        frame #8: 0x00007fff839a9df6 AppKit`-[NSView _recursiveSyncAndDisplayViewBackingSurfacesIfNeeded:] + 445
        frame #9: 0x00007fff87e2f0b6 CoreFoundation`__NSArrayEnumerate + 582
        frame #10: 0x00007fff839a9df6 AppKit`-[NSView _recursiveSyncAndDisplayViewBackingSurfacesIfNeeded:] + 445
        frame #11: 0x00007fff87e2f0b6 CoreFoundation`__NSArrayEnumerate + 582
        frame #12: 0x00007fff839a9df6 AppKit`-[NSView _recursiveSyncAndDisplayViewBackingSurfacesIfNeeded:] + 445
        frame #13: 0x00007fff87e2f0b6 CoreFoundation`__NSArrayEnumerate + 582
        frame #14: 0x00007fff839a9df6 AppKit`-[NSView _recursiveSyncAndDisplayViewBackingSurfacesIfNeeded:] + 445
        frame #15: 0x00007fff83a20683 AppKit`-[NSView _recursiveSyncDisplayAndFlushViewBackingSurfacesIfNeeded] + 96
        frame #16: 0x00007fff83970c8a AppKit`-[NSView _displayRectIgnoringOpacity:isVisibleRect:rectIsVisibleRectForView:] + 1168
        frame #17: 0x00007fff8393b993 AppKit`-[NSView displayIfNeeded] + 1830

    versus normal drawing:
        frame #0: 0x0000000100463304 NovaMind5`-[NMPresentableScroller drawRect:] + 148 at NMPresentableScroller.m:90
        frame #1: 0x00007fff83979e20 AppKit`-[NSView _drawRect:clip:] + 4437
        frame #2: 0x00007fff83976c93 AppKit`-[NSView _recursiveDisplayRectIfNeededIgnoringOpacity:isVisibleRect:rectIsVisibleRectForView:topView:] + 3058
        frame #3: 0x00007fff83977724 AppKit`-[NSView _recursiveDisplayRectIfNeededIgnoringOpacity:isVisibleRect:rectIsVisibleRectForView:topView:] + 5763
        frame #4: 0x00007fff83977724 AppKit`-[NSView _recursiveDisplayRectIfNeededIgnoringOpacity:isVisibleRect:rectIsVisibleRectForView:topView:] + 5763
        frame #5: 0x00007fff83977724 AppKit`-[NSView _recursiveDisplayRectIfNeededIgnoringOpacity:isVisibleRect:rectIsVisibleRectForView:topView:] + 5763
        frame #6: 0x00007fff83977724 AppKit`-[NSView _recursiveDisplayRectIfNeededIgnoringOpacity:isVisibleRect:rectIsVisibleRectForView:topView:] + 5763
        frame #7: 0x00007fff83977724 AppKit`-[NSView _recursiveDisplayRectIfNeededIgnoringOpacity:isVisibleRect:rectIsVisibleRectForView:topView:] + 5763
        frame #8: 0x00007fff83977724 AppKit`-[NSView _recursiveDisplayRectIfNeededIgnoringOpacity:isVisibleRect:rectIsVisibleRectForView:topView:] + 5763
        frame #9: 0x00007fff83975e23 AppKit`-[NSThemeFrame _recursiveDisplayRectIfNeededIgnoringOpacity:isVisibleRect:rectIsVisibleRectForView:topView:] + 314
        frame #10: 0x00007fff83971a3d AppKit`-[NSView _displayRectIgnoringOpacity:isVisibleRect:rectIsVisibleRectForView:] + 4675
        frame #11: 0x00007fff8393b993 AppKit`-[NSView displayIfNeeded] + 1830

    So I wondered what would happen if I used setWantsLayer:YES on my accessory view, and low and behold it is now drawn on my 10.7+ style overlay views! Yay!

    It doesn't auto-hide, and it only actually works when the scrollers are shown, so I'm not sure that it's going to be that useful anyway. I thought the auto-hiding might be done through the opacity of a backing layer on the view, but there is apparently no layer on this view (!). I logged setAlphaValue: and it wasn't called, so am not sure where the opacity is coming from.

    If I can get it to auto-hide with the scroller, and make the scrollers stay visible when the control is in use (it's sometimes a popup button, and sometimes a text field), then it may work out well. Otherwise, I may just have to stick with using either always visible scrollers, or find another location for my zoom controls.

    At least I

    Regards

    Gideon

    On 08/07/2012, at 12:21 PM, Graham Cox <graham.cox...> wrote:

    >
    > On 08/07/2012, at 12:13 PM, Gideon King wrote:
    >
    >> Yes, I was using that type of code before too, but it didn't work with the new scrollbar styles (drawing artifacts on resize, not automatically hiding), which is what prompted me to look at subclassing the scroller itself instead. Unfortunately it still only works with the legacy style scrollers and not overlays as per my previous message.
    >
    >
    > That's right. If you do anything to modify the position of the scrollers, it will revert to the legacy scrollbars. That's the way NSScroller works now. AFAIK, you can't have it both ways - add a placard and have overlay scrollers. That's why I moved my placard elsewhere. That said I never experienced drawing artefacts or other problems, it just used the old scrollbars.
    >
    > --Graham
    >
    >
  • You've totally hit what I hit when Lion first came out, and it was my big WWDC question that year - took two Apple engineers digging in the code to figure it out; turned out to be a bug lurking since the NeXT days that only gets triggered when you add subviews to the scrollview and use overlay scrollers.

    This is what we wrote at WWDC - it works on 10.6 & up:

    - (void)awakeFromNib
    {
      if (childView) {
          // Add it below the scroller so it doesn't draw over it.
          [self addSubview:childView
                positioned:NSWindowBelow
                relativeTo:[self verticalScroller]];
          [self tile];
      }
    }

    - (void)tile
    {
      BOOL isLegacy = YES;
      if ([self respondsToSelector:@selector(scrollerStyle)]) {
          isLegacy = [self scrollerStyle] == 0; // NSScrollerStyleLegacy
      }
      if (isLegacy) {
          [self legacyTile];
      } else {
          [self overlayTile];
      }
    }

    - (void)overlayTile
    {
      NSClipView *contentView = [self contentView];
      NSRect savedClipBounds = [contentView bounds];

      [super tile];
      if (!childView) {
          return;
      }

      NSRect contentFrame = [contentView frame];
      NSRect childFrame = [childView frame];
      childFrame.origin.y = NSMaxY(contentFrame) - NSHeight(childFrame);
      childFrame.size.width = contentFrame.size.width;
      [childView setFrame:childFrame];
      contentFrame.size.height = NSMinY(childFrame) - NSMinY(contentFrame);
      [contentView setFrameSize:contentFrame.size];

      // Fix adjusted scroll position.
      [contentView scrollToPoint:savedClipBounds.origin];
      [self reflectScrolledClipView:contentView];
    }

    - (void)legacyTile
    {
      if (!childView) {
          [super tile];
          return;
      }

      NSSize viewSize = [self bounds].size;
      NSRect childFrame = [childView bounds];
      NSRect contentFrame = NSZeroRect;
      NSClipView *contentView = [self contentView];

      // Adjust content for child view.
      childFrame.origin.x = childFrame.origin.y = 0;
      contentFrame.origin.x = 0;
      contentFrame.size.height = viewSize.height - childFrame.size.height;
      contentFrame.size.width = viewSize.width;

      if ([self isFlipped]) {
          childFrame.origin.y = viewSize.height - childFrame.size.height;
          contentFrame.origin.y = 0;
      } else {
          contentFrame.origin.y = childFrame.size.height;
      }

      /*
        * Adjust scrollers for the new content view size,
        * allowing for scroller space.
        */
      BOOL hasHScroll = [self hasHorizontalScroller];
      BOOL hasVScroll = [self hasVerticalScroller];
      NSScroller *verticalScroller = [self verticalScroller];
      NSScroller *horizontalScroller = [self horizontalScroller];
      NSRect insetContentFrame = contentFrame;

      NSRect hscrollRect = hasHScroll ? [horizontalScroller frame] : NSZeroRect;
      NSRect vscrollRect = hasVScroll ? [verticalScroller frame] : NSZeroRect;
      CGFloat vScrollWidth = vscrollRect.size.width;
      CGFloat hScrollHeight = hscrollRect.size.height;

      insetContentFrame.size.width -= vScrollWidth;
      insetContentFrame.size.height -= hScrollHeight;

      /*
        * Force a layout at the proposed new size so we'll know
        * whether we'll need scrollbars for the documentRect
        */
      NSSize oldContentSize = [contentView bounds].size;
      if (!NSEqualSizes(oldContentSize, insetContentFrame.size)) {
          [contentView setFrame:insetContentFrame];
      }
      NSRect docRect = [[self contentView] documentRect];

      BOOL showVScroll      hasVScroll && insetContentFrame.size.height < docRect.size.height;
      BOOL showHScroll      hasHScroll && insetContentFrame.size.width < docRect.size.width;

      if (showVScroll) {
          [verticalScroller setHidden:NO];
          vscrollRect.size.height = viewSize.height;
          vscrollRect.origin.x = viewSize.width - vscrollRect.size.width;
          vscrollRect.origin.y = 0;
          if (showHScroll) {
            vscrollRect.size.height -= hScrollHeight;
            if (![self isFlipped]) {
                vscrollRect.origin.y = hScrollHeight;
            }
          }
          [verticalScroller setFrame:vscrollRect];
          contentFrame.size.width = insetContentFrame.size.width;
      } else {
          [verticalScroller setHidden:YES];
      }

      if (showHScroll) {
          [horizontalScroller setHidden:NO];
          hscrollRect.size.width = viewSize.width;
          hscrollRect.origin.x = 0;
          if ([self isFlipped]) {
            hscrollRect.origin.y = viewSize.height - hScrollHeight;
            childFrame.origin.y -= hScrollHeight;
          } else {
            hscrollRect.origin.y = 0;
            childFrame.origin.y += hScrollHeight;
          }
          if (showVScroll) {
            hscrollRect.size.width -= vScrollWidth;
          }
          [horizontalScroller setFrame:hscrollRect];
          contentFrame.size.height = insetContentFrame.size.height;
      } else {
          [horizontalScroller setHidden:YES];
      }

      // Set the final new size.
      childFrame.size.width = contentFrame.size.width;
      [childView setFrame:childFrame];
      [contentView setFrame:contentFrame];
    }

    ----- Original Message -----
    From: "Gideon King" <gideon...>
    To: "Graham Cox" <graham.cox...>
    Cc: "Cocoa-Dev List" <cocoa-dev...>
    Sent: Saturday, July 7, 2012 7:13:53 PM
    Subject: Re: Problem adding subview to NSScroller subclass

    Yes, I was using that type of code before too, but it didn't work with the new scrollbar styles (drawing artifacts on resize, not automatically hiding), which is what prompted me to look at subclassing the scroller itself instead. Unfortunately it still only works with the legacy style scrollers and not overlays as per my previous message.

    Regards

    Gideon

    On 08/07/2012, at 11:04 AM, Graham Cox <graham.cox...> wrote:

    >
    > On 07/07/2012, at 6:38 PM, Gideon King wrote:
    >
    >> Has anybody successfully added a subview to an NSScroller?
    >
    >
    > Yes, but more recently I took it out again and moved that extra view elsewhere, because on Lion/Mountain Lion, these scroll areas are handled differently and the presence of an extra view is detected and used to revert the scroller to the "legacy" scrollbar design, which is going to look increasingly out of place as apps adopt the new one. This extra special-casing could also be affecting whether and how your view is drawn. That said my code worked on Lion, it just used the legacy scrollbars.
    >
    > My subclass of NSScrollView only overrides one method - tile, and adds a property, 'placard' which is the extra view to insert. I believe this code originally was derived from someone else's example I found on the web in about 2003, so I'm not trying to claim it as my own. It does work however.
    >
    >
    > --Graham
    >
    >
  • Excellent, thanks Lee Ann.

    From the look of the code, the accessory view will be shown all the time. I guess I have a UI decision to make as to whether to have just my zoom controls showing all the time and overlay scrollers (which may look a bit funny, but gives you maximum room), or using legacy scrollers, or moving the zoom controls somewhere else, or maybe making the view translucent until you mouse over it or something... I see that Keynote has an entire bottom bar dedicated to just a single zoom control which seems rather wasteful.

    Regards

    Gideon

    On 08/07/2012, at 3:04 PM, Lee Ann Rucker <lrucker...> wrote:

    > You've totally hit what I hit when Lion first came out, and it was my big WWDC question that year - took two Apple engineers digging in the code to figure it out; turned out to be a bug lurking since the NeXT days that only gets triggered when you add subviews to the scrollview and use overlay scrollers.
    >
    > This is what we wrote at WWDC - it works on 10.6 & up:
    > ...
  • Yes, I had an info view below my NSCollectionView, and as that avoids horizontal scrollbar and only shows the vertical one when needed, it looked really weird for the scrollbar not to reach the bottom of the window. Shouldn't take too much tweaking to support a view that can be added and removed; it does work without the secondary view loaded.

    ----- Original Message -----
    From: "Gideon King" <gideon...>
    To: "Lee Ann Rucker" <lrucker...>
    Cc: "Cocoa-Dev List" <cocoa-dev...>
    Sent: Sunday, July 8, 2012 12:23:04 AM
    Subject: Re: Problem adding subview to NSScroller subclass

    Excellent, thanks Lee Ann.

    From the look of the code, the accessory view will be shown all the time. I guess I have a UI decision to make as to whether to have just my zoom controls showing all the time and overlay scrollers (which may look a bit funny, but gives you maximum room), or using legacy scrollers, or moving the zoom controls somewhere else, or maybe making the view translucent until you mouse over it or something... I see that Keynote has an entire bottom bar dedicated to just a single zoom control which seems rather wasteful.

    Regards

    Gideon

    On 08/07/2012, at 3:04 PM, Lee Ann Rucker <lrucker...> wrote:

    > You've totally hit what I hit when Lion first came out, and it was my big WWDC question that year - took two Apple engineers digging in the code to figure it out; turned out to be a bug lurking since the NeXT days that only gets triggered when you add subviews to the scrollview and use overlay scrollers.
    >
    > This is what we wrote at WWDC - it works on 10.6 & up:
    > ...
previous month july 2012 next month
MTWTFSS
            1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31          
Go to today