chromium/ui/base/cocoa/tool_tip_base_view.mm

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#import "ui/base/cocoa/tool_tip_base_view.h"

#include "base/check.h"
#include "base/memory/raw_ptr.h"
#include "base/notreached.h"

// Below is the nasty tooltip stuff -- copied from WebKit's WebHTMLView.mm
// with minor modifications for code style and commenting.
//
//  The 'public' interface is -setToolTipAtMousePoint:. This differs from
// -setToolTip: in that the updated tooltip takes effect immediately,
//  without the user's having to move the mouse out of and back into the view.
//
// Unfortunately, doing this requires sending fake mouseEnter/Exit events to
// the view, which in turn requires overriding some internal tracking-rect
// methods (to keep track of its owner & userdata, which need to be filled out
// in the fake events.) --snej 7/6/09


/*
 * Copyright (C) 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights reserved.
 *           (C) 2006, 2007 Graham Dennis ([email protected])
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1.  Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 * 2.  Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
 *     its contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

@implementation ToolTipBaseView {
  // These are part of the magic tooltip code from WebKit's WebHTMLView:
  id __weak _trackingRectOwner;
  raw_ptr<void, DanglingUntriaged> _trackingRectUserData;
  NSTrackingRectTag _lastToolTipTag;
  NSString* __strong _toolTip;
}

#ifndef MAC_OS_VERSION_13_0
#define MAC_OS_VERSION_13_0 130000
#endif

// Remove these methods once macOS 13 becomes the minimum deployment version
// (see comment in -setToolTipAtMousePoint: below). Consider moving the
// remainder into BaseView.
#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_VERSION_13_0

// Any non-zero value will do, but using something recognizable might help us
// debug some day.
const NSTrackingRectTag kTrackingRectTag = 0xBADFACE;

// Override of a public NSView method, replacing the inherited functionality.
// See above for rationale.
- (NSTrackingRectTag)addTrackingRect:(NSRect)rect
                               owner:(id)owner
                            userData:(void *)data
                        assumeInside:(BOOL)assumeInside {
  DCHECK(_trackingRectOwner == nil);
  _trackingRectOwner = owner;
  _trackingRectUserData = data;
  return kTrackingRectTag;
}

// Override of (apparently) a private NSView method(!) See above for rationale.
- (NSTrackingRectTag)_addTrackingRect:(NSRect)rect
                                owner:(id)owner
                             userData:(void *)data
                         assumeInside:(BOOL)assumeInside
                       useTrackingNum:(int)tag {
  DCHECK(tag == 0 || tag == kTrackingRectTag);
  DCHECK(_trackingRectOwner == nil);
  _trackingRectOwner = owner;
  _trackingRectUserData = data;
  return kTrackingRectTag;
}

// Override of (apparently) a private NSView method(!) See above for rationale.
- (void)_addTrackingRects:(NSRect *)rects
                    owner:(id)owner
             userDataList:(void **)userDataList
         assumeInsideList:(BOOL *)assumeInsideList
             trackingNums:(NSTrackingRectTag *)trackingNums
                    count:(int)count {
  DCHECK(count == 1);
  DCHECK(trackingNums[0] == 0 || trackingNums[0] == kTrackingRectTag);
  DCHECK(_trackingRectOwner == nil);
  _trackingRectOwner = owner;
  _trackingRectUserData = userDataList[0];
  trackingNums[0] = kTrackingRectTag;
}

// Override of a public NSView method, replacing the inherited functionality.
// See above for rationale.
- (void)removeTrackingRect:(NSTrackingRectTag)tag {
  if (tag == 0)
    return;

  if (tag == kTrackingRectTag) {
    _trackingRectOwner = nil;
    return;
  }

  if (tag == _lastToolTipTag) {
    [super removeTrackingRect:tag];
    _lastToolTipTag = 0;
    return;
  }

  // If any other tracking rect is being removed, we don't know how it was
  // created and it's possible there's a leak involved (see Radar 3500217).
  NOTREACHED();
}

// Override of (apparently) a private NSView method(!)
- (void)_removeTrackingRects:(NSTrackingRectTag *)tags count:(int)count {
  for (int i = 0; i < count; ++i) {
    NSTrackingRectTag tag = tags[i];
    if (tag == 0)
      continue;
    DCHECK(tag == kTrackingRectTag);
    _trackingRectOwner = nil;
  }
}

// Sends a fake NSEventTypeMouseExited event to the view for its current
// tracking rect.
- (void)_sendToolTipMouseExited {
  // Nothing matters except window, trackingNumber, and userData.
  NSEvent* fakeEvent =
      [NSEvent enterExitEventWithType:NSEventTypeMouseExited
                             location:NSZeroPoint
                        modifierFlags:0
                            timestamp:NSApp.currentEvent.timestamp
                         windowNumber:self.window.windowNumber
                              context:nullptr
                          eventNumber:0
                       trackingNumber:kTrackingRectTag
                             userData:_trackingRectUserData];
  [_trackingRectOwner mouseExited:fakeEvent];
}

// Sends a fake NSEventTypeMouseEntered event to the view for its current
// tracking rect.
- (void)_sendToolTipMouseEntered {
  NSInteger windowNumber = self.window.windowNumber;

  // Only send a fake mouse enter if the mouse is actually over the window,
  // versus over a window which overlaps it (see http://crbug.com/883269).
  if ([NSWindow windowNumberAtPoint:NSEvent.mouseLocation
          belowWindowWithWindowNumber:0] != windowNumber) {
    return;
  }

  // Nothing matters except window, trackingNumber, and userData.
  NSEvent* fakeEvent =
      [NSEvent enterExitEventWithType:NSEventTypeMouseEntered
                             location:NSZeroPoint
                        modifierFlags:0
                            timestamp:NSApp.currentEvent.timestamp
                         windowNumber:windowNumber
                              context:nullptr
                          eventNumber:0
                       trackingNumber:kTrackingRectTag
                             userData:_trackingRectUserData];
  [_trackingRectOwner mouseEntered:fakeEvent];
}

#endif  // MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_VERSION_13_0

// Sets the view's current tooltip, to be displayed at the current mouse
// location. (This does not make the tooltip appear -- as usual, it only
// appears after a delay.) Pass null to remove the tooltip.
- (void)setToolTipAtMousePoint:(NSString *)string {
  NSString* toolTip = string.length == 0 ? nil : string;
  if ((toolTip && _toolTip && [toolTip isEqualToString:_toolTip]) ||
      (!toolTip && !_toolTip)) {
    return;
  }

#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_VERSION_13_0
  if (_toolTip) {
    [self _sendToolTipMouseExited];
  }
#endif  // MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_VERSION_13_0

  _toolTip = [toolTip copy];

  // It appears that as of macOS 13, tooltips are no longer set up with
  // calls to addTrackingRect:... or removeTrackingRect:. As a result,
  // _trackingRectOwner remains nil, which means the calls to
  // [_trackingRectOwner mouseEntered:] in _sendToolTipMouseEntered and
  // [_trackingRectOwner mouseExited:] in _sendToolTipMouseExited do nothing.
  // It looks like this doesn't affect tooltip display, but the call to
  // _sendToolTipMouseExited no longer orders it out. Therefore, when the user
  // moves the mouse away from a tooltip on Ventura, the call to
  // setToolTipAtMousePoint:nil initiated by the target view leaves the
  // tooltip onscreen.
  //
  // The logic below was
  //
  //   if (tooltip) {
  //     [self removeAllTooltips];
  //     ...
  //   }
  //
  // By moving the call to -removeAllTooltips outside of the conditional,
  // we can ensure any visible tooltip will be removed from the screen.
  // See crbug.com/1409942.
  //
  // The strategy of removing all tooltips rather than the single one that
  // was added comes from WebKit, like the rest of the code here. It
  // apparently works around some AppKit bug.
  [self removeAllToolTips];
  if (toolTip) {
    NSRect wideOpenRect = NSMakeRect(-100000, -100000, 200000, 200000);
    _lastToolTipTag = [self addToolTipRect:wideOpenRect
                                     owner:self
                                  userData:nullptr];
#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_VERSION_13_0
    [self _sendToolTipMouseEntered];
#endif  // MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_VERSION_13_0
  }
}

// NSView calls this to get the text when displaying the tooltip.
- (NSString *)view:(NSView *)view
  stringForToolTip:(NSToolTipTag)tag
             point:(NSPoint)point
          userData:(void *)data {
  return [_toolTip copy];
}

@end