chromium/remoting/host/file_transfer/file_chooser_mac.mm

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

#include "remoting/host/file_transfer/file_chooser.h"

#import <Cocoa/Cocoa.h>

#include <utility>

#include "base/apple/foundation_util.h"
#include "base/functional/bind.h"
#include "base/memory/weak_ptr.h"
#import "base/task/sequenced_task_runner.h"
#include "base/task/sequenced_task_runner.h"
#include "base/threading/sequence_bound.h"
#include "remoting/base/string_resources.h"
#include "ui/base/l10n/l10n_util_mac.h"

@interface FileTransferOpenPanelDelegate : NSObject <NSOpenSavePanelDelegate> {
}
- (BOOL)panel:(id)sender shouldEnableURL:(NSURL*)url;
- (BOOL)panel:(id)sender validateURL:(NSURL*)url error:(NSError**)outError;
@end

@implementation FileTransferOpenPanelDelegate
- (BOOL)panel:(id)sender shouldEnableURL:(NSURL*)url {
  return url.fileURL;
}

- (BOOL)panel:(id)sender validateURL:(NSURL*)url error:(NSError**)outError {
  // Refuse to accept users closing the dialog with a key repeat, since the key
  // may have been first pressed while the user was looking at something else.
  if (NSApp.currentEvent.type == NSEventTypeKeyDown &&
      NSApp.currentEvent.ARepeat) {
    return NO;
  }

  return YES;
}
@end

namespace remoting {

namespace {

class FileChooserMac;

class MacFileChooserOnUiThread {
 public:
  MacFileChooserOnUiThread(
      scoped_refptr<base::SequencedTaskRunner> caller_task_runner,
      base::WeakPtr<FileChooserMac> file_chooser_mac);

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

  ~MacFileChooserOnUiThread();

  void Show();

 private:
  void RunCallback(FileChooser::Result result);

  FileTransferOpenPanelDelegate* __strong delegate_;
  NSOpenPanel* __strong open_panel_;
  scoped_refptr<base::SequencedTaskRunner> caller_task_runner_;
  base::WeakPtr<FileChooserMac> file_chooser_mac_;
  base::WeakPtrFactory<MacFileChooserOnUiThread> weak_ptr_factory_;
};

class FileChooserMac : public FileChooser {
 public:
  FileChooserMac(scoped_refptr<base::SequencedTaskRunner> ui_task_runner,
                 ResultCallback callback);

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

  ~FileChooserMac() override;

  // FileChooser implementation.
  void Show() override;

  void RunCallback(FileChooser::Result result);

 private:
  FileChooser::ResultCallback callback_;
  base::SequenceBound<MacFileChooserOnUiThread> mac_file_chooser_on_ui_thread_;
  base::WeakPtrFactory<FileChooserMac> weak_ptr_factory_;
};

MacFileChooserOnUiThread::MacFileChooserOnUiThread(
    scoped_refptr<base::SequencedTaskRunner> caller_task_runner,
    base::WeakPtr<FileChooserMac> file_chooser_mac)
    : delegate_([[FileTransferOpenPanelDelegate alloc] init]),
      caller_task_runner_(std::move(caller_task_runner)),
      file_chooser_mac_(std::move(file_chooser_mac)),
      weak_ptr_factory_(this) {}

MacFileChooserOnUiThread::~MacFileChooserOnUiThread() {
  if (open_panel_) {
    // Note: In existing implementations, this synchronously invokes the
    // completion handler.
    [open_panel_ cancel:open_panel_];
  }
}

void MacFileChooserOnUiThread::Show() {
  DCHECK(!open_panel_);
  open_panel_ = [NSOpenPanel openPanel];
  open_panel_.message = l10n_util::GetNSString(IDS_DOWNLOAD_FILE_DIALOG_TITLE);
  open_panel_.allowsMultipleSelection = NO;
  open_panel_.canChooseFiles = YES;
  open_panel_.canChooseDirectories = NO;
  open_panel_.delegate = delegate_;

  // Because the MacFileChooserOnUiThread destructor calls `cancel` on the open
  // panel if it is still open, which in turn causes the completion handler to
  // be invoked synchronously, weak_this is expected always to be valid.
  // However, because `cancel` does not appear to be explicitly documented to
  // invoke the completion handler synchronously, using a weak pointer guards
  // against any hypothetical future change in behavior.
  base::WeakPtr<MacFileChooserOnUiThread> weak_this =
      weak_ptr_factory_.GetWeakPtr();
  [open_panel_ beginWithCompletionHandler:^(NSModalResponse result) {
    if (!weak_this) {
      return;
    }
    if (result == NSModalResponseOK) {
      NSURL* url = weak_this->open_panel_.URLs[0];
      if (url.fileURL) {
        weak_this->RunCallback(base::apple::NSStringToFilePath([url path]));
      } else {
        // The panel delegate should prevent the user making a selection where
        // `url.fileURL` is false, so this is unexpected.
        weak_this->RunCallback(protocol::MakeFileTransferError(
            FROM_HERE, protocol::FileTransfer_Error_Type_UNEXPECTED_ERROR));
      }
    } else {
      weak_this->RunCallback(protocol::MakeFileTransferError(
          FROM_HERE, protocol::FileTransfer_Error_Type_CANCELED));
    }
    weak_this->open_panel_ = nil;
  }];
  // Bring to front.
  [NSApp activateIgnoringOtherApps:YES];
}

void MacFileChooserOnUiThread::RunCallback(FileChooser::Result result) {
  caller_task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&FileChooserMac::RunCallback, file_chooser_mac_,
                                std::move(result)));
}

FileChooserMac::FileChooserMac(
    scoped_refptr<base::SequencedTaskRunner> ui_task_runner,
    ResultCallback callback)
    : callback_(std::move(callback)), weak_ptr_factory_(this) {
  mac_file_chooser_on_ui_thread_ =
      base::SequenceBound<MacFileChooserOnUiThread>(
          ui_task_runner, base::SequencedTaskRunner::GetCurrentDefault(),
          weak_ptr_factory_.GetWeakPtr());
}

void FileChooserMac::Show() {
  mac_file_chooser_on_ui_thread_.AsyncCall(&MacFileChooserOnUiThread::Show);
}

void FileChooserMac::RunCallback(FileChooser::Result result) {
  std::move(callback_).Run(std::move(result));
}

FileChooserMac::~FileChooserMac() = default;

}  // namespace

std::unique_ptr<FileChooser> FileChooser::Create(
    scoped_refptr<base::SequencedTaskRunner> ui_task_runner,
    ResultCallback callback) {
  return std::make_unique<FileChooserMac>(std::move(ui_task_runner),
                                          std::move(callback));
}

}  // namespace remoting