chromium/remoting/host/disconnect_window_mac.mm

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

#import "remoting/host/disconnect_window_mac.h"

#import <Cocoa/Cocoa.h>

#include <memory>

#include "base/compiler_specific.h"
#include "base/functional/bind.h"
#include "base/i18n/rtl.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/string_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "remoting/base/string_resources.h"
#include "remoting/host/client_session_control.h"
#include "remoting/host/host_window.h"
#include "ui/base/l10n/l10n_util_mac.h"

@interface DisconnectWindowController ()
- (BOOL)isRToL;
- (void)hide;
@property(nonatomic, strong) NSTextField* connectedToField;
@property(nonatomic, strong) NSButton* disconnectButton;
@end

const int kMaximumConnectedNameWidthInPixels = 600;

namespace {

bool IsDarkMode() {
  NSAppearanceName appearance =
      [NSApp.effectiveAppearance bestMatchFromAppearancesWithNames:@[
        NSAppearanceNameAqua, NSAppearanceNameDarkAqua
      ]];
  return [appearance isEqual:NSAppearanceNameDarkAqua];
}

}  // namespace

namespace remoting {

class DisconnectWindowMac : public HostWindow {
 public:
  DisconnectWindowMac();

  DisconnectWindowMac(const DisconnectWindowMac&) = delete;
  DisconnectWindowMac& operator=(const DisconnectWindowMac&) = delete;

  ~DisconnectWindowMac() override;

  // HostWindow overrides.
  void Start(const base::WeakPtr<ClientSessionControl>& client_session_control)
      override;

 private:
  DisconnectWindowController* __strong window_controller_;
};

DisconnectWindowMac::DisconnectWindowMac() = default;

DisconnectWindowMac::~DisconnectWindowMac() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  [window_controller_ hide];
  window_controller_ = nil;
}

void DisconnectWindowMac::Start(
    const base::WeakPtr<ClientSessionControl>& client_session_control) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(client_session_control);
  DCHECK(window_controller_ == nil);

  // Create the window.
  base::OnceClosure disconnect_callback =
      base::BindOnce(&ClientSessionControl::DisconnectSession,
                     client_session_control, ErrorCode::OK);
  std::string client_jid = client_session_control->client_jid();
  std::string username = client_jid.substr(0, client_jid.find('/'));

  NSRect frame = NSMakeRect(0, 0, 466, 40);
  DisconnectWindow* window =
      [[DisconnectWindow alloc] initWithContentRect:frame
                                          styleMask:NSWindowStyleMaskBorderless
                                            backing:NSBackingStoreBuffered
                                              defer:NO];
  window.releasedWhenClosed = NO;
  window_controller_ = [[DisconnectWindowController alloc]
      initWithCallback:std::move(disconnect_callback)
              username:username
                window:window];
  [window_controller_ initializeWindow];
  [window_controller_ showWindow:nil];
}

// static
std::unique_ptr<HostWindow> HostWindow::CreateDisconnectWindow() {
  return std::make_unique<DisconnectWindowMac>();
}

}  // namespace remoting

@implementation DisconnectWindowController {
  base::OnceClosure _disconnect_callback;
  std::u16string _username;
}

@synthesize connectedToField = _connectedToField;
@synthesize disconnectButton = _disconnectButton;

- (instancetype)initWithCallback:(base::OnceClosure)disconnect_callback
                        username:(const std::string&)username
                          window:(NSWindow*)window {
  self = [super initWithWindow:window];
  if (self) {
    _disconnect_callback = std::move(disconnect_callback);
    _username = base::UTF8ToUTF16(username);
  }
  return self;
}

- (IBAction)stopSharing:(id)sender {
  if (_disconnect_callback) {
    std::move(_disconnect_callback).Run();
  }
}

- (BOOL)isRToL {
  return base::i18n::IsRTL();
}

- (void)hide {
  _disconnect_callback.Reset();
  [self close];
}

- (void)initializeWindow {
  self.window.contentView =
      [[DisconnectView alloc] initWithFrame:self.window.contentView.frame];

  self.connectedToField =
      [[NSTextField alloc] initWithFrame:NSMakeRect(26, 13, 240, 14)];
  self.connectedToField.drawsBackground = NO;
  self.connectedToField.bezeled = NO;
  self.connectedToField.editable = NO;
  self.connectedToField.font = [NSFont systemFontOfSize:11];
  [self.window.contentView addSubview:self.connectedToField];

  self.disconnectButton =
      [[NSButton alloc] initWithFrame:NSMakeRect(271, 9, 182, 22)];
  self.disconnectButton.buttonType = NSButtonTypeMomentaryPushIn;
  self.disconnectButton.bezelStyle = NSBezelStyleFlexiblePush;
  self.disconnectButton.font = [NSFont systemFontOfSize:11];
  self.disconnectButton.action = @selector(stopSharing:);
  self.disconnectButton.target = self;
  [self.window.contentView addSubview:self.disconnectButton];

  self.connectedToField.stringValue =
      l10n_util::GetNSStringF(IDS_MESSAGE_SHARED, _username);
  self.disconnectButton.title = l10n_util::GetNSString(IDS_STOP_SHARING_BUTTON);

  // Resize the window dynamically based on the content.
  CGFloat oldConnectedWidth = NSWidth(self.connectedToField.bounds);
  [self.connectedToField sizeToFit];
  NSRect connectedToFrame = self.connectedToField.frame;
  CGFloat newConnectedWidth = NSWidth(connectedToFrame);

  // Set a max width for the connected to text field.
  if (newConnectedWidth > kMaximumConnectedNameWidthInPixels) {
    newConnectedWidth = kMaximumConnectedNameWidthInPixels;
    connectedToFrame.size.width = newConnectedWidth;
    self.connectedToField.frame = connectedToFrame;
  }

  CGFloat oldDisconnectWidth = NSWidth(self.disconnectButton.bounds);
  [self.disconnectButton sizeToFit];
  NSRect disconnectFrame = self.disconnectButton.frame;
  CGFloat newDisconnectWidth = NSWidth(disconnectFrame);

  // Move the disconnect button appropriately.
  disconnectFrame.origin.x += newConnectedWidth - oldConnectedWidth;
  disconnectFrame.origin.y =
      (NSHeight(self.window.contentView.frame) - NSHeight(disconnectFrame)) / 2;
  self.disconnectButton.frame = disconnectFrame;

  // Then resize the window appropriately
  NSRect windowFrame = self.window.frame;
  windowFrame.size.width += (newConnectedWidth - oldConnectedWidth +
                             newDisconnectWidth - oldDisconnectWidth);
  [self.window setFrame:windowFrame display:NO];

  if ([self isRToL]) {
    // Handle right to left case
    CGFloat buttonInset = NSWidth(windowFrame) - NSMaxX(disconnectFrame);
    CGFloat buttonTextSpacing =
        NSMinX(disconnectFrame) - NSMaxX(connectedToFrame);
    disconnectFrame.origin.x = buttonInset;
    connectedToFrame.origin.x = NSMaxX(disconnectFrame) + buttonTextSpacing;
    self.connectedToField.frame = connectedToFrame;
    self.disconnectButton.frame = disconnectFrame;
  }

  // Center the window at the bottom of the screen, above the dock (if present).
  NSRect desktopRect = NSScreen.mainScreen.visibleFrame;
  NSRect windowRect = self.window.frame;
  CGFloat x = (NSWidth(desktopRect) - NSWidth(windowRect)) / 2;
  CGFloat y = NSMinY(desktopRect);
  [self.window setFrameOrigin:NSMakePoint(x, y)];
}

- (void)windowWillClose:(NSNotification*)notification {
  [self stopSharing:self];
}

@end

@interface DisconnectWindow ()
- (BOOL)isRToL;
@end

@implementation DisconnectWindow

- (instancetype)initWithContentRect:(NSRect)contentRect
                          styleMask:(NSUInteger)aStyle
                            backing:(NSBackingStoreType)bufferingType
                              defer:(BOOL)flag {
  // Pass NSWindowStyleMaskBorderless for the styleMask to remove the title bar.
  self = [super initWithContentRect:contentRect
                          styleMask:NSWindowStyleMaskBorderless
                            backing:bufferingType
                              defer:flag];

  if (self) {
    // Set window to be clear and non-opaque so we can see through it.
    self.backgroundColor = NSColor.clearColor;
    self.opaque = NO;
    self.movableByWindowBackground = YES;

    // Pull the window up to Status Level so that it always displays.
    [self setLevel:NSStatusWindowLevel];
  }
  return self;
}

- (BOOL)isRToL {
  DCHECK([self.windowController respondsToSelector:@selector(isRToL)]);
  return [self.windowController isRToL];
}

@end

@interface DisconnectView ()
- (BOOL)isRToL;
@end

@implementation DisconnectView

- (BOOL)isRToL {
  DCHECK([self.window isKindOfClass:[DisconnectWindow class]]);
  return [static_cast<DisconnectWindow*>(self.window) isRToL];
}

- (void)drawRect:(NSRect)rect {
  // All magic numbers taken from screen shots provided by UX.
  NSRect bounds = NSInsetRect(self.bounds, 1, 1);

  NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:bounds
                                                       xRadius:5
                                                       yRadius:5];
  NSColor* bgColor;
  NSColor* frameColor;
  NSColor* lineColor;
  NSColor* lineShadowColor;
  if (IsDarkMode()) {
    bgColor = [NSColor colorWithCalibratedWhite:0.2 alpha:1.0];
    frameColor = [NSColor colorWithCalibratedWhite:0.91 alpha:1.0];
    lineColor = [NSColor colorWithCalibratedWhite:0.91 alpha:1.0];
    lineShadowColor = [NSColor colorWithCalibratedWhite:0.32 alpha:1.0];
  } else {
    bgColor = [NSColor colorWithCalibratedWhite:0.91 alpha:1.0];
    frameColor = [NSColor colorWithCalibratedRed:0.13
                                           green:0.69
                                            blue:0.11
                                           alpha:1.0];
    lineColor = [NSColor colorWithCalibratedWhite:0.70 alpha:1.0];
    lineShadowColor = [NSColor colorWithCalibratedWhite:0.97 alpha:1.0];
  }
  [bgColor setFill];
  [path fill];
  [path setLineWidth:4];
  [frameColor setStroke];
  [path stroke];

  // Draw drag handle on proper side
  const CGFloat kHeight = 21.0;
  const CGFloat kBaseInset = 12.0;
  const CGFloat kDragHandleWidth = 5.0;

  // Turn off aliasing so it's nice and crisp.
  NSGraphicsContext* context = NSGraphicsContext.currentContext;
  BOOL alias = context.shouldAntialias;
  context.shouldAntialias = NO;

  // Handle bidirectional locales properly.
  CGFloat inset = [self isRToL] ? NSMaxX(bounds) - kBaseInset - kDragHandleWidth
                                : kBaseInset;

  NSPoint top = NSMakePoint(inset, NSMidY(bounds) - kHeight / 2.0);
  NSPoint bottom = NSMakePoint(inset, top.y + kHeight);

  path = [NSBezierPath bezierPath];
  [path moveToPoint:top];
  [path lineToPoint:bottom];
  [lineColor setStroke];
  [path stroke];

  top.x += 1;
  bottom.x += 1;
  path = [NSBezierPath bezierPath];
  [path moveToPoint:top];
  [path lineToPoint:bottom];
  [lineShadowColor setStroke];
  [path stroke];

  top.x += 2;
  bottom.x += 2;
  path = [NSBezierPath bezierPath];
  [path moveToPoint:top];
  [path lineToPoint:bottom];
  [lineColor setStroke];
  [path stroke];

  top.x += 1;
  bottom.x += 1;
  path = [NSBezierPath bezierPath];
  [path moveToPoint:top];
  [path lineToPoint:bottom];
  [lineShadowColor setStroke];
  [path stroke];

  context.shouldAntialias = alias;
}

@end