chromium/remoting/host/keyboard_layout_monitor_mac.cc

// 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/keyboard_layout_monitor.h"

#include <Carbon/Carbon.h>
#include <CoreFoundation/CoreFoundation.h>
#include <CoreServices/CoreServices.h>

#include <memory>
#include <optional>
#include <string_view>
#include <utility>

#include "base/apple/scoped_cftyperef.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "remoting/proto/control.pb.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/dom/keycode_converter.h"

namespace remoting {

namespace {

class KeyboardLayoutMonitorMac : public KeyboardLayoutMonitor {
 public:
  explicit KeyboardLayoutMonitorMac(
      base::RepeatingCallback<void(const protocol::KeyboardLayout&)> callback);

  ~KeyboardLayoutMonitorMac() override;

  void Start() override;

 private:
  struct CallbackContext {
    scoped_refptr<base::SequencedTaskRunner> task_runner;
    base::WeakPtr<KeyboardLayoutMonitorMac> weak_ptr;
  };

  void OnLayoutChanged(const protocol::KeyboardLayout& new_layout);

  static void SelectedKeyboardInputSourceChangedCallback(
      CFNotificationCenterRef center,
      void* observer,
      CFNotificationName name,
      const void* object,
      CFDictionaryRef userInfo);

  static void QueryLayoutOnMainLoop(CallbackContext* callback_context);

  base::RepeatingCallback<void(const protocol::KeyboardLayout&)> callback_;
  std::unique_ptr<CallbackContext> callback_context_;
  base::WeakPtrFactory<KeyboardLayoutMonitorMac> weak_ptr_factory_;
};

std::optional<protocol::LayoutKeyFunction> GetFixedKeyFunction(int keycode);
std::optional<protocol::LayoutKeyFunction> GetCharFunction(UniChar char_code,
                                                           int keycode);

KeyboardLayoutMonitorMac::KeyboardLayoutMonitorMac(
    base::RepeatingCallback<void(const protocol::KeyboardLayout&)> callback)
    : callback_(std::move(callback)), weak_ptr_factory_(this) {}

KeyboardLayoutMonitorMac::~KeyboardLayoutMonitorMac() {
  if (!callback_context_) {
    return;
  }

  CFNotificationCenterRemoveObserver(
      CFNotificationCenterGetDistributedCenter(), callback_context_.get(),
      kTISNotifySelectedKeyboardInputSourceChanged, nullptr);

  // The distributed notification center posts all notifications from the
  // process's main run loop. Schedule deletion from the same loop to ensure
  // we don't delete the callback context while a notification is being
  // dispatched.
  // Store the callback context pointer in a local variable so the block
  // captures it directly instead of capturing this.
  CallbackContext* callback_context = callback_context_.release();
  CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopCommonModes, ^(void) {
    delete callback_context;
  });
  // No need to explicitly wake up the main loop here. It's fine if the
  // deletion is delayed slightly until the next time the main loop wakes up
  // normally.
}

void KeyboardLayoutMonitorMac::Start() {
  DCHECK(!callback_context_);
  callback_context_ = std::make_unique<CallbackContext>(
      CallbackContext{base::SequencedTaskRunner::GetCurrentDefault(),
                      weak_ptr_factory_.GetWeakPtr()});
  CFNotificationCenterAddObserver(
      CFNotificationCenterGetDistributedCenter(), callback_context_.get(),
      SelectedKeyboardInputSourceChangedCallback,
      kTISNotifySelectedKeyboardInputSourceChanged, nullptr,
      CFNotificationSuspensionBehaviorDeliverImmediately);

  // Get the initial layout.
  // Store the callback context pointer in a local variable so the block
  // captures it directly instead of capturing this.
  CallbackContext* callback_context = callback_context_.get();
  base::apple::ScopedCFTypeRef<CFRunLoopRef> main_loop(
      CFRunLoopGetMain(), base::scoped_policy::RETAIN);
  CFRunLoopPerformBlock(main_loop.get(), kCFRunLoopCommonModes, ^(void) {
    QueryLayoutOnMainLoop(callback_context);
  });
  CFRunLoopWakeUp(main_loop.get());
}

void KeyboardLayoutMonitorMac::OnLayoutChanged(
    const protocol::KeyboardLayout& new_layout) {
  callback_.Run(new_layout);
}

// static
void KeyboardLayoutMonitorMac::SelectedKeyboardInputSourceChangedCallback(
    CFNotificationCenterRef center,
    void* observer,
    CFNotificationName name,
    const void* object,
    CFDictionaryRef userInfo) {
  CallbackContext* callback_context = static_cast<CallbackContext*>(observer);
  QueryLayoutOnMainLoop(callback_context);
}

// static
void KeyboardLayoutMonitorMac::QueryLayoutOnMainLoop(
    KeyboardLayoutMonitorMac::CallbackContext* callback_context) {
  base::apple::ScopedCFTypeRef<TISInputSourceRef> input_source(
      TISCopyCurrentKeyboardLayoutInputSource());
  base::apple::ScopedCFTypeRef<CFDataRef> layout_data(
      static_cast<CFDataRef>(TISGetInputSourceProperty(
          input_source.get(), kTISPropertyUnicodeKeyLayoutData)),
      base::scoped_policy::RETAIN);

  if (!layout_data) {
    LOG(WARNING) << "Failed to query keyboard layout.";
    return;
  }

  protocol::KeyboardLayout layout_message;

  std::uint8_t keyboard_type = LMGetKbdType();
  PhysicalKeyboardLayoutType layout_type = KBGetLayoutType(keyboard_type);

  for (ui::DomCode key : KeyboardLayoutMonitor::kSupportedKeys) {
    // Lang1 and Lang2 are only present on JIS-style keyboards.
    if ((key == ui::DomCode::LANG1 || key == ui::DomCode::LANG2) &&
        layout_type != kKeyboardJIS) {
      continue;
    }

    // Skip Caps Lock until we decide how/if we want to handle it.
    if (key == ui::DomCode::CAPS_LOCK) {
      continue;
    }

    std::uint32_t usb_code = ui::KeycodeConverter::DomCodeToUsbKeycode(key);
    int keycode = ui::KeycodeConverter::DomCodeToNativeKeycode(key);

    auto& key_actions =
        *(*layout_message.mutable_keys())[usb_code].mutable_actions();

    for (int shift_level = 0; shift_level < 4; ++shift_level) {
      std::optional<protocol::LayoutKeyFunction> fixed_function =
          GetFixedKeyFunction(keycode);
      if (fixed_function) {
        key_actions[shift_level].set_function(*fixed_function);
        continue;
      }

      std::uint32_t modifier_state = 0;
      if (shift_level & 1) {
        modifier_state |= shiftKey;
      }
      if (shift_level & 2) {
        modifier_state |= optionKey;
      }
      std::uint32_t deadkey_state = 0;
      UniChar result_array[255];
      UniCharCount result_length = 0;
      UCKeyTranslate(reinterpret_cast<const UCKeyboardLayout*>(
                         CFDataGetBytePtr(layout_data.get())),
                     keycode, kUCKeyActionDown, modifier_state >> 8,
                     keyboard_type, kUCKeyTranslateNoDeadKeysMask,
                     &deadkey_state, std::size(result_array), &result_length,
                     result_array);

      if (result_length == 0) {
        continue;
      }

      if (result_length == 1) {
        std::optional<protocol::LayoutKeyFunction> char_function =
            GetCharFunction(result_array[0], keycode);
        if (char_function) {
          key_actions[shift_level].set_function(*char_function);
          continue;
        }
      }

      key_actions[shift_level].set_character(
          base::UTF16ToUTF8(std::u16string_view(
              reinterpret_cast<const char16_t*>(result_array), result_length)));
    }

    if (key_actions.size() == 0) {
      layout_message.mutable_keys()->erase(usb_code);
    }
  }

  callback_context->task_runner->PostTask(
      FROM_HERE,
      base::BindOnce(&KeyboardLayoutMonitorMac::OnLayoutChanged,
                     callback_context->weak_ptr, std::move(layout_message)));
}

std::optional<protocol::LayoutKeyFunction> GetFixedKeyFunction(int keycode) {
  // Some keys are not represented in the layout and always have the same
  // function.
  switch (keycode) {
    case kVK_Command:
    case kVK_RightCommand:
      return protocol::LayoutKeyFunction::COMMAND;
    case kVK_Shift:
    case kVK_RightShift:
      return protocol::LayoutKeyFunction::SHIFT;
    case kVK_CapsLock:
      return protocol::LayoutKeyFunction::CAPS_LOCK;
    case kVK_Option:
    case kVK_RightOption:
      return protocol::LayoutKeyFunction::OPTION;
    case kVK_Control:
    case kVK_RightControl:
      return protocol::LayoutKeyFunction::CONTROL;
    case kVK_F1:
      return protocol::LayoutKeyFunction::F1;
    case kVK_F2:
      return protocol::LayoutKeyFunction::F2;
    case kVK_F3:
      return protocol::LayoutKeyFunction::F3;
    case kVK_F4:
      return protocol::LayoutKeyFunction::F4;
    case kVK_F5:
      return protocol::LayoutKeyFunction::F5;
    case kVK_F6:
      return protocol::LayoutKeyFunction::F6;
    case kVK_F7:
      return protocol::LayoutKeyFunction::F7;
    case kVK_F8:
      return protocol::LayoutKeyFunction::F8;
    case kVK_F9:
      return protocol::LayoutKeyFunction::F9;
    case kVK_F10:
      return protocol::LayoutKeyFunction::F10;
    case kVK_F11:
      return protocol::LayoutKeyFunction::F11;
    case kVK_F12:
      return protocol::LayoutKeyFunction::F12;
    case kVK_F13:
      return protocol::LayoutKeyFunction::F13;
    case kVK_F14:
      return protocol::LayoutKeyFunction::F14;
    case kVK_F15:
      return protocol::LayoutKeyFunction::F15;
    case kVK_F16:
      return protocol::LayoutKeyFunction::F16;
    case kVK_F17:
      return protocol::LayoutKeyFunction::F17;
    case kVK_F18:
      return protocol::LayoutKeyFunction::F18;
    case kVK_F19:
      return protocol::LayoutKeyFunction::F19;
    case kVK_JIS_Kana:
      return protocol::LayoutKeyFunction::KANA;
    case kVK_JIS_Eisu:
      return protocol::LayoutKeyFunction::EISU;
    default:
      return std::nullopt;
  }
}

std::optional<protocol::LayoutKeyFunction> GetCharFunction(UniChar char_code,
                                                           int keycode) {
  switch (char_code) {
    case kHomeCharCode:
      return protocol::LayoutKeyFunction::HOME;
    case kEnterCharCode:
      // Numpad Enter
      return protocol::LayoutKeyFunction::ENTER;
    case kEndCharCode:
      return protocol::LayoutKeyFunction::END;
    case kHelpCharCode:
      // Old Macs called this key "Help". Newer Macs lack it altogether (and
      // have "Fn" in this position).
      // TODO(rkjnsn): See how the latest macOS handles this key, and consider
      // hiding this key or creating a distinct HELP function, as appropriate.
      return protocol::LayoutKeyFunction::INSERT;
    case kBackspaceCharCode:
      return protocol::LayoutKeyFunction::BACKSPACE;
    case kTabCharCode:
      return protocol::LayoutKeyFunction::TAB;
    case kPageUpCharCode:
      return protocol::LayoutKeyFunction::PAGE_UP;
    case kPageDownCharCode:
      return protocol::LayoutKeyFunction::PAGE_DOWN;
    case kReturnCharCode:
      // Writing system return key.
      return protocol::LayoutKeyFunction::ENTER;
    case kFunctionKeyCharCode:
      // The known keys with this char code are handled by GetFixedKeyFunction.
      return protocol::LayoutKeyFunction::UNKNOWN;
    case kEscapeCharCode:
      if (keycode == kVK_ANSI_KeypadClear) {
        return protocol::LayoutKeyFunction::CLEAR;
      }
      return protocol::LayoutKeyFunction::ESCAPE;
    case kLeftArrowCharCode:
      return protocol::LayoutKeyFunction::ARROW_LEFT;
    case kRightArrowCharCode:
      return protocol::LayoutKeyFunction::ARROW_RIGHT;
    case kUpArrowCharCode:
      return protocol::LayoutKeyFunction::ARROW_UP;
    case kDownArrowCharCode:
      return protocol::LayoutKeyFunction::ARROW_DOWN;
    case kDeleteCharCode:
      return protocol::LayoutKeyFunction::DELETE_;
    default:
      return std::nullopt;
  }
}

}  // namespace

std::unique_ptr<KeyboardLayoutMonitor> KeyboardLayoutMonitor::Create(
    base::RepeatingCallback<void(const protocol::KeyboardLayout&)> callback,
    scoped_refptr<base::SingleThreadTaskRunner> input_task_runner) {
  return std::make_unique<KeyboardLayoutMonitorMac>(std::move(callback));
}

}  // namespace remoting