| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/base/ime/win/on_screen_keyboard_display_manager_tab_tip.h" |
| |
| #include <windows.h> |
| |
| #include <shellapi.h> |
| #include <shlobj.h> |
| #include <shobjidl.h> // Must be before propkey. |
| |
| #include "base/bind.h" |
| #include "base/location.h" |
| #include "base/logging.h" |
| #include "base/single_thread_task_runner.h" |
| #include "base/strings/string_util.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "base/win/registry.h" |
| #include "base/win/scoped_co_mem.h" |
| #include "base/win/win_util.h" |
| #include "base/win/windows_version.h" |
| #include "ui/base/ime/input_method_keyboard_controller_observer.h" |
| #include "ui/base/win/hidden_window.h" |
| #include "ui/display/win/screen_win.h" |
| #include "ui/gfx/geometry/dip_util.h" |
| |
| namespace { |
| |
| constexpr base::TimeDelta kCheckOSKDelay = |
| base::TimeDelta::FromMilliseconds(1000); |
| constexpr base::TimeDelta kDismissKeyboardRetryTimeout = |
| base::TimeDelta::FromMilliseconds(100); |
| constexpr int kDismissKeyboardMaxRetries = 5; |
| |
| constexpr wchar_t kOSKClassName[] = L"IPTip_Main_Window"; |
| |
| constexpr wchar_t kWindows8OSKRegPath[] = |
| L"Software\\Classes\\CLSID\\{054AAE20-4BEA-4347-8A35-64A533254A9D}" |
| L"\\LocalServer32"; |
| |
| } // namespace |
| |
| namespace ui { |
| |
| // This class provides functionality to detect when the on screen keyboard |
| // is displayed and move the main window up if it is obscured by the keyboard. |
| class OnScreenKeyboardDetector { |
| public: |
| OnScreenKeyboardDetector( |
| OnScreenKeyboardDisplayManagerTabTip* display_manager); |
| ~OnScreenKeyboardDetector(); |
| |
| // Schedules a delayed task which detects if the on screen keyboard was |
| // displayed. |
| void DetectKeyboard(HWND main_window); |
| |
| // Dismisses the on screen keyboard. If a call to display the keyboard was |
| // made, this function waits for the keyboard to become visible by retrying |
| // upto a maximum of kDismissKeyboardMaxRetries. |
| void DismissKeyboard(); |
| |
| // Returns true if the osk is visible. |
| static bool IsKeyboardVisible(); |
| |
| private: |
| // Returns the occluded rect in dips. |
| gfx::Rect GetOccludedRect(); |
| |
| // Executes as a task and detects if the on screen keyboard is displayed. |
| // Once the keyboard is displayed it schedules the HideIfNecessary() task to |
| // detect when the keyboard is or should be hidden. |
| void CheckIfKeyboardVisible(); |
| |
| // Executes as a task and detects if the keyboard was hidden or should be |
| // hidden. |
| void HideIfNecessary(); |
| |
| // Notifies observers that the keyboard was displayed. |
| // A recurring task HideIfNecessary() is started to detect when the OSK |
| // disappears. |
| void HandleKeyboardVisible(const gfx::Rect& occluded_rect); |
| |
| // Notifies observers that the keyboard was hidden. |
| // The observer list is cleared out after this notification. |
| void HandleKeyboardHidden(); |
| |
| OnScreenKeyboardDisplayManagerTabTip* display_manager_; |
| |
| // The main window which displays the on screen keyboard. |
| HWND main_window_ = nullptr; |
| |
| // Tracks if the keyboard was displayed. |
| bool osk_visible_notification_received_ = false; |
| |
| // Set to true if a call to DetectKeyboard() was made. |
| bool keyboard_detect_requested_ = false; |
| |
| // Contains the number of attempts made to dismiss the keyboard. Please refer |
| // to the DismissKeyboard() function for more information. |
| int keyboard_dismiss_retry_count_ = 0; |
| |
| // Should be the last member in the class. Helps ensure that tasks spawned |
| // by this class instance are canceled when it is destroyed. |
| base::WeakPtrFactory<OnScreenKeyboardDetector> keyboard_detector_factory_; |
| |
| DISALLOW_COPY_AND_ASSIGN(OnScreenKeyboardDetector); |
| }; |
| |
| // OnScreenKeyboardDetector member definitions. |
| OnScreenKeyboardDetector::OnScreenKeyboardDetector( |
| OnScreenKeyboardDisplayManagerTabTip* display_manager) |
| : display_manager_(display_manager), keyboard_detector_factory_(this) {} |
| |
| OnScreenKeyboardDetector::~OnScreenKeyboardDetector() {} |
| |
| void OnScreenKeyboardDetector::DetectKeyboard(HWND main_window) { |
| main_window_ = main_window; |
| keyboard_detect_requested_ = true; |
| // The keyboard is displayed by TabTip.exe which is launched via a |
| // ShellExecute call in the |
| // OnScreenKeyboardDisplayManager::DisplayVirtualKeyboard() function. We use |
| // a delayed task to check if the keyboard is visible because of the possible |
| // delay between the ShellExecute call and the keyboard becoming visible. |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&OnScreenKeyboardDetector::CheckIfKeyboardVisible, |
| keyboard_detector_factory_.GetWeakPtr()), |
| kCheckOSKDelay); |
| } |
| |
| void OnScreenKeyboardDetector::DismissKeyboard() { |
| // We dismiss the virtual keyboard by generating the SC_CLOSE. |
| HWND osk = ::FindWindow(kOSKClassName, nullptr); |
| if (::IsWindow(osk) && ::IsWindowEnabled(osk)) { |
| keyboard_detect_requested_ = false; |
| keyboard_dismiss_retry_count_ = 0; |
| HandleKeyboardHidden(); |
| PostMessage(osk, WM_SYSCOMMAND, SC_CLOSE, 0); |
| return; |
| } |
| |
| if (keyboard_detect_requested_) { |
| if (keyboard_dismiss_retry_count_ < kDismissKeyboardMaxRetries) { |
| keyboard_dismiss_retry_count_++; |
| // Please refer to the comments in the DetectKeyboard() function for more |
| // information as to why we need a delayed task here. |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce( |
| base::IgnoreResult(&OnScreenKeyboardDetector::DismissKeyboard), |
| keyboard_detector_factory_.GetWeakPtr()), |
| kDismissKeyboardRetryTimeout); |
| } else { |
| keyboard_dismiss_retry_count_ = 0; |
| } |
| } |
| } |
| |
| // static |
| bool OnScreenKeyboardDetector::IsKeyboardVisible() { |
| HWND osk = ::FindWindow(kOSKClassName, nullptr); |
| if (!::IsWindow(osk)) |
| return false; |
| return ::IsWindowVisible(osk) && ::IsWindowEnabled(osk); |
| } |
| |
| gfx::Rect OnScreenKeyboardDetector::GetOccludedRect() { |
| gfx::Rect occluded_rect; |
| HWND osk = ::FindWindow(kOSKClassName, nullptr); |
| if (!::IsWindow(osk) || !::IsWindowVisible(osk) || !::IsWindowEnabled(osk)) |
| return occluded_rect; |
| |
| RECT osk_rect = {}; |
| RECT main_window_rect = {}; |
| if (!::GetWindowRect(osk, &osk_rect) || |
| !::GetWindowRect(main_window_, &main_window_rect)) { |
| return occluded_rect; |
| } |
| |
| gfx::Rect gfx_osk_rect(osk_rect); |
| gfx::Rect gfx_main_window_rect(main_window_rect); |
| |
| gfx_osk_rect.Intersect(gfx_main_window_rect); |
| |
| return display::win::ScreenWin::ScreenToDIPRect(main_window_, gfx_osk_rect); |
| } |
| |
| void OnScreenKeyboardDetector::CheckIfKeyboardVisible() { |
| gfx::Rect occluded_rect = GetOccludedRect(); |
| if (!occluded_rect.IsEmpty()) { |
| if (!osk_visible_notification_received_) |
| HandleKeyboardVisible(occluded_rect); |
| } else { |
| DVLOG(1) << "OSK did not come up. Something wrong."; |
| } |
| } |
| |
| void OnScreenKeyboardDetector::HideIfNecessary() { |
| HWND osk = ::FindWindow(kOSKClassName, nullptr); |
| if (!::IsWindow(osk)) |
| return; |
| |
| // Three cases here. |
| // 1. OSK was hidden because the user dismissed it. |
| // 2. We are no longer in the foreground. |
| // 3. The OSK is still visible. |
| // In the first case we just have to notify the observers that the OSK was |
| // hidden. |
| // In the second case we need to dismiss the OSK which internally will |
| // notify the observers about the OSK being hidden. |
| if (!::IsWindowEnabled(osk)) { |
| if (osk_visible_notification_received_) { |
| if (main_window_ == ::GetForegroundWindow()) { |
| DVLOG(1) << "OSK window hidden while we are in the foreground."; |
| HandleKeyboardHidden(); |
| } |
| } |
| } else if (main_window_ != ::GetForegroundWindow()) { |
| if (osk_visible_notification_received_) { |
| DVLOG(1) << "We are no longer in the foreground. Dismising OSK."; |
| DismissKeyboard(); |
| } |
| } else { |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&OnScreenKeyboardDetector::HideIfNecessary, |
| keyboard_detector_factory_.GetWeakPtr()), |
| kCheckOSKDelay); |
| } |
| } |
| |
| void OnScreenKeyboardDetector::HandleKeyboardVisible( |
| const gfx::Rect& occluded_rect) { |
| DCHECK(!osk_visible_notification_received_); |
| osk_visible_notification_received_ = true; |
| |
| display_manager_->NotifyKeyboardVisible(occluded_rect); |
| |
| // Now that the keyboard is visible, run the task to detect if it was hidden. |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&OnScreenKeyboardDetector::HideIfNecessary, |
| keyboard_detector_factory_.GetWeakPtr()), |
| kCheckOSKDelay); |
| } |
| |
| void OnScreenKeyboardDetector::HandleKeyboardHidden() { |
| osk_visible_notification_received_ = false; |
| display_manager_->NotifyKeyboardHidden(); |
| } |
| |
| // OnScreenKeyboardDisplayManagerTabTip member definitions. |
| OnScreenKeyboardDisplayManagerTabTip::OnScreenKeyboardDisplayManagerTabTip( |
| HWND hwnd) |
| : hwnd_(hwnd) { |
| DCHECK_GE(base::win::GetVersion(), base::win::VERSION_WIN8); |
| } |
| |
| OnScreenKeyboardDisplayManagerTabTip::~OnScreenKeyboardDisplayManagerTabTip() {} |
| |
| bool OnScreenKeyboardDisplayManagerTabTip::DisplayVirtualKeyboard() { |
| if (base::win::IsKeyboardPresentOnSlate(nullptr, ui::GetHiddenWindow())) |
| return false; |
| |
| if (osk_path_.empty() && !GetOSKPath(&osk_path_)) { |
| DLOG(WARNING) << "Failed to get on screen keyboard path from registry"; |
| return false; |
| } |
| |
| HINSTANCE ret = ::ShellExecuteW(nullptr, L"", osk_path_.c_str(), nullptr, |
| nullptr, SW_SHOW); |
| |
| bool success = reinterpret_cast<intptr_t>(ret) > 32; |
| if (success) { |
| // If multiple calls to DisplayVirtualKeyboard occur one after the other, |
| // the last observer would be the one to get notifications. |
| keyboard_detector_ = std::make_unique<OnScreenKeyboardDetector>(this); |
| keyboard_detector_->DetectKeyboard(hwnd_); |
| } |
| return success; |
| } |
| |
| void OnScreenKeyboardDisplayManagerTabTip::DismissVirtualKeyboard() { |
| if (keyboard_detector_) |
| keyboard_detector_->DismissKeyboard(); |
| } |
| |
| void OnScreenKeyboardDisplayManagerTabTip::AddObserver( |
| InputMethodKeyboardControllerObserver* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void OnScreenKeyboardDisplayManagerTabTip::RemoveObserver( |
| InputMethodKeyboardControllerObserver* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| bool OnScreenKeyboardDisplayManagerTabTip::GetOSKPath( |
| base::string16* osk_path) { |
| DCHECK(osk_path); |
| |
| // We need to launch TabTip.exe from the location specified under the |
| // LocalServer32 key for the {{054AAE20-4BEA-4347-8A35-64A533254A9D}} |
| // CLSID. |
| // TabTip.exe is typically found at |
| // c:\program files\common files\microsoft shared\ink on English Windows. |
| // We don't want to launch TabTip.exe from |
| // c:\program files (x86)\common files\microsoft shared\ink. This path is |
| // normally found on 64 bit Windows. |
| base::win::RegKey key(HKEY_LOCAL_MACHINE, kWindows8OSKRegPath, |
| KEY_READ | KEY_WOW64_64KEY); |
| DWORD osk_path_length = 1024; |
| if (key.ReadValue(nullptr, base::WriteInto(osk_path, osk_path_length), |
| &osk_path_length, nullptr) != ERROR_SUCCESS) { |
| return false; |
| } |
| |
| osk_path->resize(base::string16::traits_type::length(osk_path->c_str())); |
| |
| *osk_path = base::ToLowerASCII(*osk_path); |
| |
| size_t common_program_files_offset = osk_path->find(L"%commonprogramfiles%"); |
| // Typically the path to TabTip.exe read from the registry will start with |
| // %CommonProgramFiles% which needs to be replaced with the corrsponding |
| // expanded string. |
| // If the path does not begin with %CommonProgramFiles% we use it as is. |
| if (common_program_files_offset != base::string16::npos) { |
| // Preserve the beginning quote in the path. |
| osk_path->erase(common_program_files_offset, |
| wcslen(L"%commonprogramfiles%")); |
| // The path read from the registry contains the %CommonProgramFiles% |
| // environment variable prefix. On 64 bit Windows the SHGetKnownFolderPath |
| // function returns the common program files path with the X86 suffix for |
| // the FOLDERID_ProgramFilesCommon value. |
| // To get the correct path to TabTip.exe we first read the environment |
| // variable CommonProgramW6432 which points to the desired common |
| // files path. Failing that we fallback to the SHGetKnownFolderPath API. |
| |
| // We then replace the %CommonProgramFiles% value with the actual common |
| // files path found in the process. |
| base::string16 common_program_files_path; |
| DWORD buffer_size = |
| GetEnvironmentVariable(L"CommonProgramW6432", nullptr, 0); |
| if (buffer_size) { |
| GetEnvironmentVariable( |
| L"CommonProgramW6432", |
| base::WriteInto(&common_program_files_path, buffer_size), |
| buffer_size); |
| DCHECK(!common_program_files_path.empty()); |
| } else { |
| base::win::ScopedCoMem<wchar_t> common_program_files; |
| if (FAILED(SHGetKnownFolderPath(FOLDERID_ProgramFilesCommon, 0, nullptr, |
| &common_program_files))) { |
| return false; |
| } |
| common_program_files_path = common_program_files; |
| } |
| osk_path->insert(common_program_files_offset, common_program_files_path); |
| } |
| return !osk_path->empty(); |
| } |
| |
| bool OnScreenKeyboardDisplayManagerTabTip::IsKeyboardVisible() { |
| return OnScreenKeyboardDetector::IsKeyboardVisible(); |
| } |
| |
| void OnScreenKeyboardDisplayManagerTabTip::NotifyKeyboardVisible( |
| const gfx::Rect& occluded_rect) { |
| for (InputMethodKeyboardControllerObserver& observer : observers_) |
| observer.OnKeyboardVisible(occluded_rect); |
| } |
| |
| void OnScreenKeyboardDisplayManagerTabTip::NotifyKeyboardHidden() { |
| for (InputMethodKeyboardControllerObserver& observer : observers_) |
| observer.OnKeyboardHidden(); |
| } |
| |
| } // namespace ui |