chromium/ui/shell_dialogs/auto_close_dialog_event_handler_win.cc

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

#include "ui/shell_dialogs/auto_close_dialog_event_handler_win.h"

#include <windows.h>

#include "base/check.h"
#include "base/memory/raw_ptr.h"
#include "base/threading/thread_checker.h"

namespace ui {

AutoCloseDialogEventHandler::AutoCloseDialogEventHandler(HWND owner_window)
    : owner_window_(owner_window) {
  CHECK(!instance_);
  instance_ = this;

  CHECK(owner_window_);
}

AutoCloseDialogEventHandler::~AutoCloseDialogEventHandler() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  CHECK(instance_);
  instance_ = nullptr;

  if (event_hook_) {
    ::UnhookWinEvent(event_hook_);
  }
}

HRESULT AutoCloseDialogEventHandler::Initialize(IFileDialog* file_dialog) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  CHECK(!initialize_called_);
  initialize_called_ = true;

  CHECK(!event_hook_);
  CHECK(!dialog_window_);

  Microsoft::WRL::ComPtr<IOleWindow> ole_window;
  HRESULT hr = file_dialog->QueryInterface(IID_PPV_ARGS(&ole_window));
  if (FAILED(hr)) {
    return hr;
  }

  HWND dialog_window;
  hr = ole_window->GetWindow(&dialog_window);
  if (FAILED(hr)) {
    return hr;
  }

  // Get the process id and the thread id of the owner window to limit the
  // scope of the event hook.
  DWORD process_id = 0;
  DWORD thread_id = ::GetWindowThreadProcessId(owner_window_, &process_id);
  if (!process_id || !thread_id) {
    return E_FAIL;
  }

  // `SetWinEventHook` is used to be notified when the owner window is closed.
  // See https://devblogs.microsoft.com/oldnewthing/20111026-00/?p=9263
  CHECK(!event_hook_);
  event_hook_ = ::SetWinEventHook(EVENT_OBJECT_DESTROY, EVENT_OBJECT_DESTROY,
                                  nullptr, &EventHookCallback, process_id,
                                  thread_id, WINEVENT_OUTOFCONTEXT);
  if (!event_hook_) {
    return E_FAIL;
  }

  dialog_window_ = dialog_window;
  return S_OK;
}

void AutoCloseDialogEventHandler::OnWindowDestroyedNotification(HWND window) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  // Ignore unrelated notifications.
  if (window != owner_window_) {
    return;
  }

  // IFileDialog::Close() expects to be called from a IFileDialogEvents callback
  // so it can't be used here. Send WM_CLOSE instead.
  ::PostMessage(dialog_window_, WM_CLOSE, 0, 0);
}

// static
void CALLBACK
AutoCloseDialogEventHandler::EventHookCallback(HWINEVENTHOOK handle,
                                               DWORD event,
                                               HWND hwnd,
                                               LONG id_object,
                                               LONG id_child,
                                               DWORD event_thread,
                                               DWORD event_time) {
  CHECK(event == EVENT_OBJECT_DESTROY);

  // Only care about window objects.
  if (id_object != OBJID_WINDOW) {
    return;
  }

  // This is safe thread-wise because WINEVENT_OUTOFCONTEXT guarantee that the
  // hook callback will be invoked on the same thread that set the hook.
  CHECK(instance_);
  instance_->OnWindowDestroyedNotification(hwnd);
}

HRESULT AutoCloseDialogEventHandler::OnTypeChange(IFileDialog* file_dialog) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  // OnTypeChange will be invoked multiple times during the lifecycle of the
  // dialog. Only do the initialization once.
  if (initialize_called_) {
    return S_OK;
  }

  return Initialize(file_dialog);
}

HRESULT AutoCloseDialogEventHandler::OnFileOk(IFileDialog*) {
  return E_NOTIMPL;
}
HRESULT AutoCloseDialogEventHandler::OnFolderChange(IFileDialog*) {
  return E_NOTIMPL;
}
HRESULT AutoCloseDialogEventHandler::OnFolderChanging(IFileDialog*,
                                                      IShellItem*) {
  return E_NOTIMPL;
}
HRESULT AutoCloseDialogEventHandler::OnSelectionChange(IFileDialog*) {
  return E_NOTIMPL;
}
HRESULT AutoCloseDialogEventHandler::OnShareViolation(
    IFileDialog*,
    IShellItem*,
    FDE_SHAREVIOLATION_RESPONSE*) {
  return E_NOTIMPL;
}
HRESULT AutoCloseDialogEventHandler::OnOverwrite(IFileDialog*,
                                                 IShellItem*,
                                                 FDE_OVERWRITE_RESPONSE*) {
  return E_NOTIMPL;
}

// static
raw_ptr<AutoCloseDialogEventHandler> AutoCloseDialogEventHandler::instance_ =
    nullptr;

}  // namespace ui