| // Copyright 2010-2015, Google Inc. |
| // All rights reserved. |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are |
| // met: |
| // |
| // * Redistributions of source code must retain the above copyright |
| // notice, this list of conditions and the following disclaimer. |
| // * Redistributions in binary form must reproduce the above |
| // copyright notice, this list of conditions and the following disclaimer |
| // in the documentation and/or other materials provided with the |
| // distribution. |
| // * Neither the name of Google Inc. nor the names of its |
| // contributors may be used to endorse or promote products derived from |
| // this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| #include "win32/ime/ime_ui_window.h" |
| |
| #define _ATL_NO_AUTOMATIC_NAMESPACE |
| #define _WTL_NO_AUTOMATIC_NAMESPACE |
| #define _ATL_NO_HOSTING |
| // Workaround against KB813540 |
| #include <atlbase_mozc.h> |
| #include <atlapp.h> |
| #include <atlstr.h> |
| #include <atlwin.h> |
| #include <atlmisc.h> |
| #include <strsafe.h> |
| |
| #include <memory> |
| |
| #include "base/const.h" |
| #include "base/logging.h" |
| #include "base/process.h" |
| #include "base/process_mutex.h" |
| #include "base/run_level.h" |
| #include "base/scoped_handle.h" |
| #include "base/singleton.h" |
| #include "base/win_util.h" |
| #include "client/client_interface.h" |
| #include "config/config_handler.h" |
| #include "renderer/renderer_command.pb.h" |
| #include "renderer/win32/win32_renderer_client.h" |
| #include "session/output_util.h" |
| #include "win32/base/conversion_mode_util.h" |
| #include "win32/base/indicator_visibility_tracker.h" |
| #include "win32/base/imm_util.h" |
| #include "win32/base/string_util.h" |
| #include "win32/base/win32_window_util.h" |
| #include "win32/ime/ime_core.h" |
| #include "win32/ime/ime_impl_imm.h" |
| #include "win32/ime/ime_language_bar.h" |
| #include "win32/ime/ime_scoped_context.h" |
| #include "win32/ime/ime_types.h" |
| #include "win32/ime/ime_ui_context.h" |
| #include "win32/ime/ime_ui_visibility_tracker.h" |
| |
| namespace mozc { |
| namespace win32 { |
| namespace { |
| |
| using ATL::CRegKey; |
| using ATL::CStringA; |
| using ATL::CWindow; |
| using WTL::CPoint; |
| using WTL::CRect; |
| |
| using ::mozc::renderer::win32::Win32RendererClient; |
| using ::std::unique_ptr; |
| |
| // True if the the DLL received DLL_PROCESS_DETACH notification. |
| volatile bool g_module_unloaded = false; |
| |
| // As filed in b/3088049 or b/4271156, the IME module (e.g. GIMEJa.ime) is |
| // sometimes unloaded too early. You can use this macro to guard callback |
| // functions from being called in such situation. |
| #define DANGLING_CALLBACK_GUARD(return_code) \ |
| do { \ |
| if (g_module_unloaded) { \ |
| return (return_code); \ |
| } \ |
| } while (false) |
| |
| // A global variable of mozc::once_t, which is POD, has no bad side effect. |
| static once_t g_launch_set_default_dialog = MOZC_ONCE_INIT; |
| |
| void LaunchSetDefaultDialog() { |
| const config::Config &config = config::ConfigHandler::GetConfig(); |
| if (config.has_check_default() && !config.check_default()) { |
| // User opted out the default IME checking. Do nothing. |
| return; |
| } |
| |
| if (ImeUtil::IsDefault()) { |
| // Mozc has already been the default IME. Do nothing. |
| return; |
| } |
| |
| { |
| mozc::ProcessMutex mutex("set_default_dialog"); |
| if (!mutex.Lock()) { |
| VLOG(1) << "SetDefaultDialog is already launched"; |
| return; |
| } |
| // mutex is unlocked here |
| } |
| |
| VLOG(1) << "Launching SetDefaultDialog"; |
| // Even if SetDefaultDialog is launched multiple times, |
| // it's safe because mozc_tool also checks the existence of |
| // the process with ProcessMutex. |
| mozc::Process::SpawnMozcProcess(mozc::kMozcTool, |
| "--mode=set_default_dialog"); |
| } |
| |
| bool IsProcessSandboxedImpl() { |
| bool is_restricted = false; |
| if (!WinUtil::IsProcessRestricted(::GetCurrentProcess(), &is_restricted)) { |
| return true; |
| } |
| if (is_restricted) { |
| return true; |
| } |
| |
| bool in_appcontainer = false; |
| if (!WinUtil::IsProcessInAppContainer(::GetCurrentProcess(), |
| &in_appcontainer)) { |
| return true; |
| } |
| |
| return in_appcontainer; |
| } |
| |
| bool IsProcessSandboxed() { |
| // Thread safety is not required. |
| static bool sandboxed = IsProcessSandboxedImpl(); |
| return sandboxed; |
| } |
| |
| // This class is expected to be used a singleton object to enable Win32 |
| // message-based event callback from the renderer to the client mainly |
| // to support mouse operation on the candidate list. |
| class PrivateRendererMessageInitializer { |
| public: |
| PrivateRendererMessageInitializer() |
| : private_renderer_message_( |
| ::RegisterWindowMessage(mozc::kMessageReceiverMessageName)) {} |
| |
| // Adds an exceptional rule for the message filter which prevents from |
| // receiving a window message from a process in lower integrity level. |
| // Returns true if the operation completed successfully. Make sure to |
| // call this method once per window, otherwise this function returns false. |
| bool Initialize(HWND target_window) { |
| if (private_renderer_message_ == 0) { |
| return false; |
| } |
| if (!::IsWindow(target_window)) { |
| return false; |
| } |
| return WindowUtil::ChangeMessageFilter( |
| target_window, private_renderer_message_); |
| } |
| |
| // Returns true if the specified message ID is the callback message. |
| bool IsPrivateRendererMessage(UINT message) const { |
| if (private_renderer_message_ == 0) { |
| return false; |
| } |
| return (private_renderer_message_ == message); |
| } |
| |
| private: |
| UINT private_renderer_message_; |
| DISALLOW_COPY_AND_ASSIGN(PrivateRendererMessageInitializer); |
| }; |
| |
| void UpdateCommand(const UIContext &context, |
| HWND ui_window, |
| const UIVisibilityTracker &ui_visibility_tracker, |
| mozc::commands::RendererCommand *command) { |
| typedef ::mozc::commands::RendererCommand::ApplicationInfo ApplicationInfo; |
| typedef ::mozc::commands::RendererCommand_IndicatorInfo IndicatorInfo; |
| typedef ::mozc::commands::CompositionMode CompositionMode; |
| |
| const bool show_composition_window = |
| ui_visibility_tracker.IsCompositionWindowVisible(); |
| const bool show_candidate_window = |
| ui_visibility_tracker.IsCandidateWindowVisible(); |
| const bool show_suggest_window = |
| ui_visibility_tracker.IsSuggestWindowVisible(); |
| |
| vector<wstring> candidate_list; |
| DWORD focused_index = 0; |
| if (!context.IsEmpty() && context.GetOpenStatus()) { |
| // Copy the last output. |
| context.GetLastOutput(command->mutable_output()); |
| |
| bool composition_window_visible = false; |
| bool candidate_window_visible = false; |
| bool suggest_window_visible = false; |
| |
| // Check if composition window is actually visible. |
| if (command->output().has_preedit()) { |
| composition_window_visible = show_composition_window; |
| } |
| |
| // Check if suggest window and candidate window are actually visible. |
| if (command->output().has_candidates() && |
| command->output().candidates().has_category()) { |
| switch (command->output().candidates().category()) { |
| case commands::SUGGESTION: |
| suggest_window_visible = show_suggest_window; |
| break; |
| case commands::CONVERSION: |
| case commands::PREDICTION: |
| candidate_window_visible = show_candidate_window; |
| break; |
| default: |
| // do nothing. |
| break; |
| } |
| } |
| |
| if (composition_window_visible || candidate_window_visible || |
| suggest_window_visible) { |
| command->set_visible(true); |
| } |
| } |
| |
| const CWindow target_window(context.GetAttachedWindow()); |
| ApplicationInfo &app_info = *command->mutable_application_info(); |
| app_info.set_process_id(::GetCurrentProcessId()); |
| app_info.set_thread_id(::GetCurrentThreadId()); |
| app_info.set_target_window_handle( |
| reinterpret_cast<uint32>(target_window.m_hWnd)); |
| app_info.set_receiver_handle(reinterpret_cast<uint32>(ui_window)); |
| app_info.set_input_framework(ApplicationInfo::IMM32); |
| int visibility = ApplicationInfo::ShowUIDefault; |
| if (show_composition_window) { |
| // Note that |ApplicationInfo::ShowCompositionWindow| represents that the |
| // application does not mind the IME showing its own composition window. |
| // This bit does not mean that |command| requires the composition window. |
| visibility |= ApplicationInfo::ShowCompositionWindow; |
| } |
| if (show_candidate_window) { |
| // Note that |ApplicationInfo::ShowCandidateWindow| represents that the |
| // application does not mind the IME showing its own candidate window. |
| // This bit does not mean that |command| requires the suggest window. |
| visibility |= ApplicationInfo::ShowCandidateWindow; |
| } |
| if (show_suggest_window) { |
| // Note that |ApplicationInfo::ShowCandidateWindow| represents that the |
| // application does not mind the IME showing its own candidate window. |
| // This bit does not mean that |command| requires the suggest window. |
| visibility |= ApplicationInfo::ShowSuggestWindow; |
| } |
| app_info.set_ui_visibilities(visibility); |
| |
| // Honor visibility bits for UI-less mode. |
| if (visibility != 0 && context.IsModeIndicatorEnabled()) { |
| IndicatorVisibilityTracker *indicator_tracker = |
| context.indicator_visibility_tracker(); |
| if (indicator_tracker != nullptr) { |
| if (indicator_tracker->IsVisible()) { |
| DWORD native_mode = 0; |
| CompositionMode mode = commands::DIRECT; |
| if (context.GetConversionMode(&native_mode) && |
| ConversionModeUtil::ToMozcMode( |
| native_mode, |
| &mode)) { |
| if (!command->has_output()) { |
| context.GetLastOutput(command->mutable_output()); |
| } |
| command->set_visible(true); |
| IndicatorInfo *info = app_info.mutable_indicator_info(); |
| info->mutable_status()->set_activated(context.GetOpenStatus()); |
| info->mutable_status()->set_mode(mode); |
| } |
| } |
| } |
| } |
| |
| context.FillFontInfo(&app_info); |
| context.FillCaretInfo(&app_info); |
| context.FillCompositionForm(&app_info); |
| context.FillCandidateForm(&app_info); |
| // UIContext::FillCharPosition is subject to cause b/3208669, b/3096191, |
| // b/3212271, b/3223011, and b/4285222. |
| // So we do not retrieve IMM32 related positional information when the |
| // renderer hides all the UI windows. |
| if (command->visible()) { |
| context.FillCharPosition(&app_info); |
| } |
| } |
| |
| // Returns a HIMC assuming from the handle of a UI window. |
| // Returns nullptr if it is not available. |
| HIMC GetSafeHIMC(HWND window_handle) { |
| if (!::IsWindow(window_handle)) { |
| return nullptr; |
| } |
| |
| const HIMC himc = |
| reinterpret_cast<HIMC>(::GetWindowLongPtr(window_handle, IMMGWLP_IMC)); |
| |
| // As revealed in b/3207711, ImeSetActiveContext is called without any |
| // prior call to ImeSelect. Perhaps this horrible situation might come from |
| // CUAS's implementation in XP. |
| // We will not use a HIMC as long as it seems to be uninitialized. |
| if (!ImeCore::IsInputContextInitialized(himc)) { |
| return nullptr; |
| } |
| |
| return himc; |
| } |
| |
| bool TurnOnIMEAndTryToReconvertFromIME(HWND hwnd) { |
| const HIMC himc = GetSafeHIMC(hwnd); |
| if (himc == nullptr) { |
| return false; |
| } |
| if (!mozc::win32::ImeCore::TurnOnIMEAndTryToReconvertFromIME(himc)) { |
| return false; |
| } |
| return true; |
| } |
| |
| class LangBarCallbackImpl : public LangBarCallback { |
| public: |
| explicit LangBarCallbackImpl(HWND hwnd) |
| : hwnd_(hwnd), |
| reference_count_(1) { |
| } |
| |
| virtual ~LangBarCallbackImpl() { |
| } |
| |
| virtual ULONG AddRef() { |
| const LONG count = ::InterlockedIncrement(&reference_count_); |
| return max(count, 0); |
| } |
| |
| virtual ULONG Release() { |
| const LONG count = ::InterlockedDecrement(&reference_count_); |
| if (count <= 0) { |
| delete this; |
| return 0; |
| } |
| return count; |
| } |
| |
| // This method is called back by the lang bar when an item on the langbar is |
| // selected. |
| virtual HRESULT OnMenuSelect(MenuId menu_id) { |
| DANGLING_CALLBACK_GUARD(E_FAIL); |
| if (::IsWindow(hwnd_) == FALSE) { |
| return E_FAIL; |
| } |
| // TODO(yukawa): Check run level. |
| HRESULT result = S_OK; |
| switch (menu_id) { |
| case LangBarCallback::kDirect: { |
| result = SetInputMode(mozc::commands::DIRECT); |
| break; |
| } |
| case LangBarCallback::kHiragana: { |
| result = SetInputMode(mozc::commands::HIRAGANA); |
| break; |
| } |
| case LangBarCallback::kFullKatakana: { |
| result = SetInputMode(mozc::commands::FULL_KATAKANA); |
| break; |
| } |
| case LangBarCallback::kHalfAlphanumeric: { |
| result = SetInputMode(mozc::commands::HALF_ASCII); |
| break; |
| } |
| case LangBarCallback::kFullAlphanumeric: { |
| result = SetInputMode(mozc::commands::FULL_ASCII); |
| break; |
| } |
| case LangBarCallback::kHalfKatakana: { |
| result = SetInputMode(mozc::commands::HALF_KATAKANA); |
| break; |
| } |
| case LangBarCallback::kProperty: { |
| // Open the config dialog. |
| if (!mozc::Process::SpawnMozcProcess( |
| mozc::kMozcTool, "--mode=config_dialog")) { |
| result = E_FAIL; |
| } |
| break; |
| } |
| case LangBarCallback::kDictionary: { |
| // Open the dictionary tool. |
| if (!mozc::Process::SpawnMozcProcess( |
| mozc::kMozcTool, "--mode=dictionary_tool")) { |
| result = E_FAIL; |
| } |
| break; |
| } |
| case LangBarCallback::kWordRegister: { |
| // Open the word register dialog. |
| if (!mozc::Process::SpawnMozcProcess( |
| mozc::kMozcTool, "--mode=word_register_dialog")) { |
| result = E_FAIL; |
| } |
| break; |
| } |
| case LangBarCallback::kHandWriting: { |
| // Open the Hand Writing Tool. |
| if (!mozc::Process::SpawnMozcProcess( |
| mozc::kMozcTool, "--mode=hand_writing")) { |
| result = E_FAIL; |
| } |
| break; |
| } |
| case LangBarCallback::kCharacterPalette: { |
| // Open the Character Palette dialog. |
| if (!mozc::Process::SpawnMozcProcess( |
| mozc::kMozcTool, "--mode=character_palette")) { |
| result = E_FAIL; |
| } |
| break; |
| } |
| case LangBarCallback::kReconversion: { |
| if (!TurnOnIMEAndTryToReconvertFromIME(hwnd_)) { |
| result = E_FAIL; |
| } |
| break; |
| } |
| case LangBarCallback::kAbout: { |
| // Open the about dialog. |
| if (!mozc::Process::SpawnMozcProcess(mozc::kMozcTool, |
| "--mode=about_dialog")) { |
| result = E_FAIL; |
| } |
| break; |
| } |
| case LangBarCallback::kHelp: { |
| // Open the about dialog. |
| const char kHelpUrl[] = "http://www.google.com/support/ime/japanese"; |
| if (!mozc::Process::OpenBrowser(kHelpUrl)) { |
| result = E_FAIL; |
| } |
| break; |
| } |
| default: { |
| break; |
| } |
| } |
| return result; |
| } |
| |
| private: |
| HRESULT SetInputMode(mozc::commands::CompositionMode mode) { |
| const HIMC himc = GetSafeHIMC(hwnd_); |
| if (himc == nullptr) { |
| return E_FAIL; |
| } |
| if (mode == mozc::commands::DIRECT) { |
| // Close IME. |
| if (::ImmSetOpenStatus(himc, FALSE) == FALSE) { |
| return E_FAIL; |
| } |
| return S_OK; |
| } |
| |
| const bool is_open = (::ImmGetOpenStatus(himc) != FALSE); |
| if (!is_open) { |
| if (::ImmSetOpenStatus(himc, TRUE) == FALSE) { |
| return E_FAIL; |
| } |
| } |
| |
| const UIContext context(himc); |
| uint32 imm32_composition_mode = 0; |
| if (!win32::ConversionModeUtil::ToNativeMode( |
| mode, context.IsKanaInputPreferred(), |
| &imm32_composition_mode)) { |
| return E_FAIL; |
| } |
| |
| DWORD composition_mode = 0; |
| DWORD sentence_mode = 0; |
| if (::ImmGetConversionStatus(himc, &composition_mode, &sentence_mode) == |
| FALSE) { |
| return E_FAIL; |
| } |
| composition_mode = static_cast<DWORD>(imm32_composition_mode); |
| DWORD visible_composition_mode = 0; |
| DWORD logical_composition_mode = 0; |
| if (context.GetVisibleConversionMode(&visible_composition_mode) && |
| context.GetLogicalConversionMode(&logical_composition_mode) && |
| (composition_mode != visible_composition_mode) && |
| (composition_mode == logical_composition_mode)) { |
| // The visible conversion mode is different from the selected mode but the |
| // actual conversion mode is the same to the selected mode. In this case, |
| // ImmSetConversionStatus cannot be suitable because the actual conversion |
| // mode will not be changed. So we will send SwitchInputMode command |
| // explicitly. |
| mozc::win32::ImeCore::SwitchInputMode(himc, composition_mode, true); |
| } else if (::ImmSetConversionStatus( |
| himc, composition_mode, sentence_mode) == FALSE) { |
| return E_FAIL; |
| } |
| return S_OK; |
| } |
| |
| // Represents the reference count to an instance. |
| // volatile modifier is added to conform with InterlockedIncrement API. |
| volatile LONG reference_count_; |
| HWND hwnd_; |
| |
| DISALLOW_COPY_AND_ASSIGN(LangBarCallbackImpl); |
| }; |
| |
| // TODO(yukawa): Refactor for unit tests and better integration with ImeCore. |
| class DefaultUIWindow { |
| public: |
| explicit DefaultUIWindow(HWND hwnd) |
| : hwnd_(hwnd), |
| langbar_callback_(new LangBarCallbackImpl(hwnd)), |
| language_bar_(new LanguageBar), |
| has_pending_langbar_update_(false) { |
| } |
| |
| ~DefaultUIWindow() { |
| langbar_callback_->Release(); |
| langbar_callback_ = nullptr; |
| } |
| |
| void UninitLangBar() { |
| CancelDeferredLangBarUpdateIfExists(); |
| language_bar_->UninitLanguageBar(); |
| } |
| |
| void OnStartComposition(const UIContext &context) { |
| context.ui_visibility_tracker()->OnStartComposition(); |
| } |
| |
| void OnComposition( |
| const UIContext &context, wchar_t latest_change, |
| const CompositionChangeAttributes &attributes) { |
| context.ui_visibility_tracker()->OnComposition(); |
| } |
| |
| void OnEndComposition(const UIContext &context) { |
| context.ui_visibility_tracker()->OnEndComposition(); |
| } |
| |
| LRESULT OnNotify(const UIContext &context, |
| DWORD sub_message, LPARAM lParam) { |
| context.ui_visibility_tracker()->OnNotify(sub_message, lParam); |
| |
| LRESULT result = 0; |
| switch (sub_message) { |
| case IMN_CLOSESTATUSWINDOW: |
| break; |
| case IMN_OPENSTATUSWINDOW: |
| break; |
| case IMN_SETCONVERSIONMODE: |
| UpdateIndicator(context); |
| result = (UpdateLangBar(context, kDeferred) ? 0 : 1); |
| break; |
| case IMN_SETSENTENCEMODE: |
| // Do nothing because we only support IME_SMODE_PHRASEPREDICT, which |
| // is not shown in our LangBar. |
| // See b/2913510, b/2954777, and b/2955175 for details. |
| break; |
| case IMN_SETOPENSTATUS: |
| UpdateIndicator(context); |
| result = (UpdateLangBar(context, kDeferred) ? 0 : 1); |
| break; |
| case IMN_SETCANDIDATEPOS: { |
| if (lParam & 0x1) { |
| UpdateCandidate(context, kMoveFocusedWindow); |
| } |
| break; |
| } |
| case IMN_SETCOMPOSITIONFONT: |
| if (!context.IsEmpty() && context.GetOpenStatus()) { |
| LOGFONT font; |
| if (context.GetCompositionFont(&font)) { |
| // TODO(yukawa): |
| // - Update the composition window. |
| } |
| } |
| break; |
| case IMN_SETCOMPOSITIONWINDOW: |
| // TODO(yukawa): Use message hook instead. |
| UpdateCandidate(context, kMoveFocusedWindow); |
| break; |
| case IMN_SETSTATUSWINDOWPOS: |
| // TODO(yukawa): |
| // - Redraw status window. |
| break; |
| case IMN_GUIDELINE: |
| break; |
| case IMN_PRIVATE: |
| if (lParam == kNotifyUpdateUI) { |
| UpdateLangBar(context, kDeferred); |
| UpdateCandidate(context, kNoEvent); |
| } else if (lParam == kNotifyReconvertFromIME) { |
| TurnOnIMEAndTryToReconvertFromIME(hwnd_); |
| } else if (lParam == kNotifyDelayedCallback) { |
| SetMozcEventCallbackTimer(context); |
| } |
| break; |
| } |
| return result; |
| } |
| |
| LRESULT OnSetContext(const UIContext &context, bool activated, |
| const ShowUIAttributes &show_ui_attributes) { |
| // |context| might be uninitialized. See b/3099588. |
| if (context.ui_visibility_tracker() == nullptr) { |
| return 0; |
| } |
| |
| if (activated) { |
| // The input context specified with |context| is activated. |
| context.ui_visibility_tracker()->OnSetContext(show_ui_attributes); |
| } |
| UpdateCandidate(context, activated ? kNoEvent : kDissociateContext); |
| if (activated) { |
| // Invalidate the LangBar state cache because the actual state of LangBar |
| // can be changed by other IMEs or applications. |
| InvalidateLangBarInfoCache(); |
| } |
| UpdateLangBar(context, kImmediate); |
| return 0; |
| } |
| |
| LRESULT OnControl(const UIContext &context, DWORD sub_message, |
| void *data) { |
| return 0; |
| } |
| |
| void OnCompositionFull(const UIContext &context) { |
| } |
| |
| void OnSelect(const UIContext &context, bool select, |
| HKL keyboard_layout) { |
| if (!select) { |
| UninitLangBar(); |
| return; |
| } |
| UpdateCandidate(context, kNoEvent); |
| UpdateLangBar(context, kImmediate); |
| |
| // If the application does not allow the IME to show any UI component, |
| // it would be better not to show the set default dialog. |
| // We use the visibility of suggest window as a launch condition of |
| // SetDefaultDialog. |
| if (context.ui_visibility_tracker()->IsSuggestWindowVisible()) { |
| if (!IsProcessSandboxed() && RunLevel::IsValidClientRunLevel()) { |
| CallOnce(&g_launch_set_default_dialog, |
| &LaunchSetDefaultDialog); |
| } |
| } |
| } |
| |
| LRESULT OnRequest(const UIContext &context, |
| WPARAM wParam, LPARAM lParam) { |
| return 0; |
| } |
| |
| LRESULT OnSessionCommand( |
| HIMC himc, commands::SessionCommand::CommandType command_type, |
| LPARAM lParam) { |
| if (himc == nullptr) { |
| return 0; |
| } |
| |
| if ((command_type != commands::SessionCommand::SELECT_CANDIDATE) && |
| (command_type != commands::SessionCommand::HIGHLIGHT_CANDIDATE) && |
| (command_type != commands::SessionCommand::USAGE_STATS_EVENT)) { |
| // Unsupported command. |
| return 0; |
| } |
| |
| if ((command_type == commands::SessionCommand::SELECT_CANDIDATE) || |
| (command_type == commands::SessionCommand::HIGHLIGHT_CANDIDATE)) { |
| // Convert |mozc_candidate_id| to candidate index. |
| const int32 mozc_candidate_id = static_cast<int32>(lParam); |
| int candidate_index = 0; |
| { |
| UIContext context(himc); |
| commands::Output output; |
| if (!context.GetLastOutput(&output)) { |
| return 0; |
| } |
| if (!OutputUtil::GetCandidateIndexById( |
| output, mozc_candidate_id, &candidate_index)) { |
| return 0; |
| } |
| } // release |context|. |
| |
| const int kCandidateWindowIndex = 0; |
| if (::ImmNotifyIME(himc, NI_SELECTCANDIDATESTR, kCandidateWindowIndex, |
| candidate_index) == FALSE) { |
| return 0; |
| } |
| if (command_type == commands::SessionCommand::SELECT_CANDIDATE) { |
| if (::ImmNotifyIME(himc, NI_CLOSECANDIDATE, kCandidateWindowIndex, 0) == |
| FALSE) { |
| return 0; |
| } |
| } |
| } else if (command_type == commands::SessionCommand::USAGE_STATS_EVENT) { |
| // Send USAGE_STATS_EVENT to the server. |
| UIContext context(himc); |
| mozc::commands::Output output; |
| mozc::commands::SessionCommand command; |
| mozc::commands::SessionCommand::UsageStatsEvent event = |
| static_cast<mozc::commands::SessionCommand::UsageStatsEvent>(lParam); |
| command.set_type(mozc::commands::SessionCommand::USAGE_STATS_EVENT); |
| command.set_usage_stats_event(event); |
| if (!context.client()->SendCommand(command, &output)) { |
| return 0; |
| } |
| } |
| return 1; |
| } |
| |
| LRESULT UIMessageProc(const UIContext &context, |
| UINT message, |
| WPARAM wParam, |
| LPARAM lParam) { |
| // A UI window should admit receiving a message even when the context is |
| // empty. You could see this situation as follows. |
| // 1. Do not set Mozc as default. |
| // 2. Open Notepad. |
| // 3. Open Help - Version Info. |
| // 4. Select Mozc in Langbar. |
| // See b/2973431 and b/2970662 for details. |
| if (context.IsEmpty()) { |
| if ((message == WM_IME_SELECT) && (wParam == FALSE)) { |
| UninitLangBar(); |
| } else { |
| UpdateLangBar(context, kDeferred); |
| } |
| return 0; |
| } |
| switch (message) { |
| case WM_IME_COMPOSITION: |
| // return value will be ignored. |
| OnComposition(context, (wchar_t)(wParam), |
| CompositionChangeAttributes(lParam)); |
| break; |
| case WM_IME_COMPOSITIONFULL: |
| // return value will be ignored. |
| OnCompositionFull(context); |
| break; |
| case WM_IME_CONTROL: |
| return OnControl(context, static_cast<DWORD>(wParam), |
| reinterpret_cast<void *>(lParam)); |
| case WM_IME_ENDCOMPOSITION: |
| // return value will be ignored. |
| OnEndComposition(context); |
| break; |
| case WM_IME_NOTIFY: |
| return OnNotify(context, static_cast<DWORD>(wParam), lParam); |
| case WM_IME_REQUEST: |
| return OnRequest(context, wParam, lParam); |
| case WM_IME_SELECT: |
| // return value will be ignored. |
| OnSelect(context, wParam != FALSE, (HKL)lParam); |
| break; |
| case WM_IME_SETCONTEXT: |
| return OnSetContext(context, wParam != FALSE, ShowUIAttributes(lParam)); |
| case WM_IME_STARTCOMPOSITION: |
| // return value will be ignored. |
| OnStartComposition(context); |
| break; |
| case WM_IME_CHAR: |
| case WM_IME_KEYDOWN: |
| case WM_IME_KEYUP: |
| // return value will be ignored. |
| break; |
| default: |
| // Unknown IME_* message |
| break; |
| } |
| // default return value |
| return 0; |
| } |
| |
| void OnTimer(WPARAM event_id) { |
| switch (event_id) { |
| case kMozcEventCallbackTimerId: |
| OnDeferredMozcEventCallback(); |
| break; |
| case kDeferredLangBarUpdateTimerId: |
| OnDeferredUpdateLangBar(); |
| break; |
| } |
| } |
| |
| private: |
| // Timer for the delayed callback to Mozc server. |
| static const UINT_PTR kMozcEventCallbackTimerId = 1; |
| |
| // Timer for the langbar update. |
| static const UINT_PTR kDeferredLangBarUpdateTimerId = 2; |
| static const DWORD kLangBarUpdateDelayMilliSec = 50; |
| |
| enum LangBarUpdateMode { |
| kDeferred = 0, |
| kImmediate = 1, |
| }; |
| |
| enum IndicatorEventType { |
| kNoEvent, |
| kMoveFocusedWindow, |
| kDissociateContext, |
| }; |
| |
| struct LangBarInfo { |
| LangBarInfo() |
| : enabled(false), |
| mode(commands::DIRECT) {} |
| |
| bool enabled; |
| commands::CompositionMode mode; |
| }; |
| |
| // Sets the timer that send callback command. |
| void SetMozcEventCallbackTimer(const UIContext &context) { |
| commands::Output output; |
| context.GetLastOutput(&output); |
| if (output.has_callback() && output.callback().has_delay_millisec()) { |
| ::SetTimer(hwnd_, |
| kMozcEventCallbackTimerId, |
| output.callback().delay_millisec(), |
| NULL); |
| } |
| } |
| |
| // Constructs RendererCommand based on various parameters in the input |
| // context. This implementation is very experimental, should be revised. |
| void UpdateCandidate(const UIContext &context, |
| IndicatorEventType indicator_event_type) { |
| if (indicator_event_type != kNoEvent) { |
| // We need to send UI event to the renderer anyway. |
| // So the returned value from |tracker| will be ignored. |
| IndicatorVisibilityTracker *tracker = |
| context.indicator_visibility_tracker(); |
| if (tracker != nullptr) { |
| switch (indicator_event_type) { |
| case kMoveFocusedWindow: |
| tracker->OnMoveFocusedWindow(); |
| break; |
| case kDissociateContext: |
| tracker->OnDissociateContext(); |
| break; |
| } |
| } |
| } |
| |
| commands::RendererCommand command; |
| command.set_type(commands::RendererCommand::UPDATE); |
| command.set_visible(false); |
| UpdateCommand(context, hwnd_, *context.ui_visibility_tracker(), |
| &command); |
| Win32RendererClient::OnUpdated(command); |
| } |
| |
| void UpdateIndicator(const UIContext &context) { |
| IndicatorVisibilityTracker *tracker = |
| context.indicator_visibility_tracker(); |
| if (tracker == nullptr) { |
| return; |
| } |
| const IndicatorVisibilityTracker::Action action = |
| tracker->OnChangeInputMode(); |
| if (action == IndicatorVisibilityTracker::kUpdateUI) { |
| commands::RendererCommand command; |
| command.set_type(commands::RendererCommand::UPDATE); |
| // Initialize as invisible just in case. Basically this flag will |
| // be set to true in UpdateCommand. |
| command.set_visible(false); |
| UpdateCommand(context, hwnd_, *context.ui_visibility_tracker(), |
| &command); |
| Win32RendererClient::OnUpdated(command); |
| } |
| } |
| |
| bool UpdateLangBar(const UIContext &context, LangBarUpdateMode update_mode) { |
| bool enabled = false; |
| commands::CompositionMode mode = commands::DIRECT; |
| |
| if (context.IsEmpty()) { |
| enabled = false; |
| mode = commands::DIRECT; |
| } else if (!context.GetOpenStatus()) { |
| // Closed |
| enabled = true; |
| mode = commands::DIRECT; |
| } else { |
| DWORD imm32_visible_mode = 0; |
| if (!context.GetVisibleConversionMode(&imm32_visible_mode)) { |
| return false; |
| } |
| commands::CompositionMode mozc_mode = commands::HIRAGANA; |
| if (!win32::ConversionModeUtil::ToMozcMode(imm32_visible_mode, |
| &mozc_mode)) { |
| return false; |
| } |
| enabled = true; |
| mode = mozc_mode; |
| } |
| |
| switch (update_mode) { |
| case kDeferred: |
| SetDeferredLangBarUpdate(true, mode); |
| break; |
| case kImmediate: |
| UpdateLangBarAndCancelUpdateTimer(enabled, mode); |
| break; |
| default: |
| DCHECK(false) << "Unknown mode: " << update_mode; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void InvalidateLangBarInfoCache() { |
| langbar_info_cache_.reset(nullptr); |
| } |
| |
| void SetDeferredLangBarUpdate(bool enabled, commands::CompositionMode mode) { |
| CancelDeferredLangBarUpdateIfExists(); |
| |
| deferred_langbar_update_request_.enabled = enabled; |
| deferred_langbar_update_request_.mode = mode; |
| const auto result = ::SetTimer( |
| hwnd_, kDeferredLangBarUpdateTimerId, |
| kLangBarUpdateDelayMilliSec, nullptr); |
| if (result != 0) { |
| has_pending_langbar_update_ = true; |
| } |
| } |
| |
| void CancelDeferredLangBarUpdateIfExists() { |
| if (!has_pending_langbar_update_) { |
| return; |
| } |
| ::KillTimer(hwnd_, kDeferredLangBarUpdateTimerId); |
| has_pending_langbar_update_ = false; |
| } |
| |
| void UpdateLangBarAndCancelUpdateTimer(bool enabled, |
| commands::CompositionMode mode) { |
| CancelDeferredLangBarUpdateIfExists(); |
| |
| // Ensure the LangBar is initialized. |
| language_bar_->InitLanguageBar(langbar_callback_); |
| |
| const bool is_redundant_call = (langbar_info_cache_.get() != nullptr && |
| langbar_info_cache_->enabled == enabled && |
| langbar_info_cache_->mode == mode); |
| |
| if (!is_redundant_call) { |
| language_bar_->SetLangbarMenuEnabled(enabled); |
| language_bar_->UpdateLangbarMenu(mode); |
| } |
| |
| if (langbar_info_cache_.get() == nullptr) { |
| langbar_info_cache_.reset(new LangBarInfo); |
| } |
| langbar_info_cache_->enabled = enabled; |
| langbar_info_cache_->mode = mode; |
| } |
| |
| void OnDeferredUpdateLangBar() { |
| UpdateLangBarAndCancelUpdateTimer(deferred_langbar_update_request_.enabled, |
| deferred_langbar_update_request_.mode); |
| } |
| |
| void OnDeferredMozcEventCallback() { |
| ::KillTimer(hwnd_, kMozcEventCallbackTimerId); |
| const HIMC himc = GetSafeHIMC(hwnd_); |
| const bool generate_message = mozc::win32::ImeCore::IsActiveContext(himc); |
| ImeCore::SendCallbackCommand(himc, generate_message); |
| } |
| |
| HWND hwnd_; |
| // TODO(yukawa): Make a wrapper class to encapsulate LangBar implementation |
| // including cache mechanism to reduce API calls. |
| unique_ptr<LanguageBar> language_bar_; |
| LangBarCallbackImpl *langbar_callback_; |
| // Represents the LangBarInfo that should be set to the LangBar when deferred |
| // timer is fired. |
| LangBarInfo deferred_langbar_update_request_; |
| // True while the deferred timer that updates the LangBar is scheduled. |
| bool has_pending_langbar_update_; |
| // Represents the last LangBarInfo that is set to the LangBar. nullpter if |
| // no cached data is available. |
| unique_ptr<LangBarInfo> langbar_info_cache_; |
| |
| DISALLOW_COPY_AND_ASSIGN(DefaultUIWindow); |
| }; |
| |
| // When a series of private callback messages is incoming from the renderer |
| // process, we might want to aggregate them mainly for performance. |
| // We can aggregate successive callbacks as follows. |
| // |
| // [Case 1] |
| // (Post Message Queue top) |
| // [1] kMessageReceiverMessageName / HIGHLIGHT_CANDIDATE |
| // [2] kMessageReceiverMessageName / HIGHLIGHT_CANDIDATE |
| // [3] kMessageReceiverMessageName / HIGHLIGHT_CANDIDATE |
| // [4] kMessageReceiverMessageName / SELECT_CANDIDATE |
| // [5] kMessageReceiverMessageName / HIGHLIGHT_CANDIDATE |
| // any other message(s) |
| // [N] kMessageReceiverMessageName / SELECT_CANDIDATE |
| // any other message(s) |
| // |
| // In this case, messages from [1] to [3] can be removed and and start |
| // handling the message [4] as if it the handler just received it. |
| // |
| // |
| // [Case 2] |
| // (Post Message Queue top) |
| // [1] kMessageReceiverMessageName / HIGHLIGHT_CANDIDATE |
| // [2] kMessageReceiverMessageName / HIGHLIGHT_CANDIDATE |
| // [3] kMessageReceiverMessageName / HIGHLIGHT_CANDIDATE |
| // any other message(s) |
| // [N] kMessageReceiverMessageName / HIGHLIGHT_CANDIDATE |
| // any other message(s) |
| // |
| // In this case, messages from [1] to [2] can be removed and and start |
| // handling the message [4] as if it the handler just received it. |
| // |
| // |
| // [Case 3] |
| // (Post Message Queue top) |
| // [1] kMessageReceiverMessageName / SELECT_CANDIDATE |
| // [2] kMessageReceiverMessageName / HIGHLIGHT_CANDIDATE |
| // any other message(s) |
| // [N] kMessageReceiverMessageName / HIGHLIGHT_CANDIDATE |
| // any other message(s) |
| // |
| // In this case, just start handling the message [1]. |
| // |
| // This function returns the aggregated message which should be handled now. |
| MSG AggregateRendererCallbackMessage( |
| HWND hwnd, UINT private_message, WPARAM wParam, LPARAM lParam) { |
| MSG current_msg = {}; |
| current_msg.hwnd = hwnd; |
| current_msg.message = private_message; |
| current_msg.wParam = wParam; |
| current_msg.lParam = lParam; |
| |
| while (true) { |
| MSG next_msg = {}; |
| // Preview the next message from the post message queue. |
| // You can avoid message dispatching from the send message queue by not |
| // specifying PM_QS_SENDMESSAGE to the 5th argument. |
| if (::PeekMessageW(&next_msg, hwnd, 0, 0, |
| PM_NOREMOVE | PM_QS_POSTMESSAGE | PM_NOYIELD) == 0) { |
| // No message is in the queue. |
| // |current_msg| is what we should handle now. |
| return current_msg; |
| } |
| |
| if (next_msg.message != private_message) { |
| // The next message is not a private renderer callback message. |
| // |current_msg| is what we should handle now. |
| return current_msg; |
| } |
| |
| // OK, the next message is a private renderer callback. |
| // Remove this message from the post message queue. |
| MSG removed_msg = {}; |
| if (::PeekMessageW(&removed_msg, hwnd, private_message, private_message, |
| PM_REMOVE | PM_QS_POSTMESSAGE | PM_NOYIELD) == 0) { |
| // Something wrong. |
| // give up aggregating the message. |
| return current_msg; |
| } |
| |
| current_msg = next_msg; |
| const mozc::commands::SessionCommand::CommandType command_type = |
| static_cast<mozc::commands::SessionCommand::CommandType>( |
| current_msg.wParam); |
| |
| // If this is a HIGHLIGHT_CANDIDATE message, resume aggregation. |
| if (command_type == |
| mozc::commands::SessionCommand::HIGHLIGHT_CANDIDATE) { |
| continue; |
| } |
| |
| return current_msg; |
| } |
| } |
| |
| LRESULT WINAPI UIWindowProc(HWND hwnd, |
| UINT message, |
| WPARAM wParam, |
| LPARAM lParam) { |
| DANGLING_CALLBACK_GUARD(0); |
| |
| const bool is_ui_message = |
| (::ImmIsUIMessage(nullptr, message, wParam, lParam) != FALSE); |
| |
| // Create UI window object and associate it to the window. |
| if (message == WM_NCCREATE) { |
| if (mozc::win32::IsInLockdownMode() || |
| !mozc::RunLevel::IsValidClientRunLevel()) { |
| // Clear Kana-lock state not to prevent users from typing their |
| // correct passwords. |
| // TODO(yukawa): Move this code to somewhere appropriate. |
| BYTE keyboard_state[256] = {}; |
| if (::GetKeyboardState(keyboard_state) != FALSE) { |
| keyboard_state[VK_KANA] = 0; |
| ::SetKeyboardState(keyboard_state); |
| } |
| |
| // Return FALSE not to be activated if the current session is |
| // WinLogon. It may reduce the risk of BSOD. |
| return FALSE; |
| } |
| |
| DefaultUIWindow* ui_window = new DefaultUIWindow(hwnd); |
| |
| ::SetWindowLongPtr(hwnd, |
| IMMGWLP_PRIVATE, |
| reinterpret_cast<LONG_PTR>(ui_window)); |
| |
| Singleton<PrivateRendererMessageInitializer>::get()->Initialize(hwnd); |
| } |
| // Retrieves UI window object from the private area. |
| DefaultUIWindow* ui_window = reinterpret_cast<DefaultUIWindow*>( |
| ::GetWindowLongPtrW(hwnd, IMMGWLP_PRIVATE)); |
| |
| if (ui_window == nullptr) { |
| if (is_ui_message) { |
| return 0; |
| } else { |
| return ::DefWindowProc(hwnd, message, wParam, lParam); |
| } |
| } |
| |
| LRESULT result = 0; |
| const bool is_renderer_message = |
| Singleton<PrivateRendererMessageInitializer>::get()-> |
| IsPrivateRendererMessage(message); |
| |
| bool is_handled = false; |
| if (is_ui_message) { |
| const HIMC himc = GetSafeHIMC(hwnd); |
| result = ui_window->UIMessageProc(UIContext(himc), |
| message, wParam, lParam); |
| is_handled = true; |
| } else if (is_renderer_message) { |
| const MSG renderer_msg = AggregateRendererCallbackMessage( |
| hwnd, message, wParam, lParam); |
| const mozc::commands::SessionCommand::CommandType command_type = |
| static_cast<mozc::commands::SessionCommand::CommandType>( |
| renderer_msg.wParam); |
| const HIMC himc = GetSafeHIMC(hwnd); |
| result = ui_window->OnSessionCommand(himc, command_type, |
| renderer_msg.lParam); |
| is_handled = true; |
| } else if (message == WM_DESTROY) { |
| // Ensure the LangBar is uninitialized. |
| ui_window->UninitLangBar(); |
| } else if (message == WM_NCDESTROY) { |
| Win32RendererClient::OnUIThreadUninitialized(); |
| |
| // Delete UI window object if the window is destroyed. |
| ::SetWindowLongPtr(hwnd, IMMGWLP_PRIVATE, 0); |
| delete ui_window; |
| } else if (message == WM_TIMER) { |
| ui_window->OnTimer(wParam); |
| // In order to reduce the potential risk of shatter attack, we don't want |
| // to pass the WM_TIMER to ::DefWindowProc. |
| is_handled = true; |
| } |
| if (!is_handled) { |
| result = ::DefWindowProcW(hwnd, message, wParam, lParam); |
| is_handled = true; |
| } |
| return result; |
| } |
| |
| } // namespace |
| |
| bool UIWindowManager::OnDllProcessAttach(HINSTANCE module_handle, |
| bool static_loading) { |
| WNDCLASSEXW wc = {}; |
| wc.cbSize = sizeof(WNDCLASSEX); |
| wc.style = CS_IME; |
| wc.lpfnWndProc = UIWindowProc; |
| wc.cbWndExtra = 2 * sizeof(LONG_PTR); |
| wc.hInstance = module_handle; |
| wc.lpszClassName = kIMEUIWndClassName; |
| |
| const ATOM atom = ::RegisterClassExW(&wc); |
| if (atom == INVALID_ATOM) { |
| const DWORD error = ::GetLastError(); |
| return false; |
| } |
| |
| Win32RendererClient::OnModuleLoaded(module_handle); |
| return true; |
| } |
| |
| void UIWindowManager::OnDllProcessDetach(HINSTANCE module_handle, |
| bool process_shutdown) { |
| if (::UnregisterClass(kIMEUIWndClassName, module_handle) == 0) { |
| // Sometimes the IME DLL is unloaded before all the UI message windows |
| // which belong to the DLL are destroyed. In such a situation, we cannot |
| // unregister window class. See b/4271156. |
| } |
| // This flag is used to inactivate out DefWindowProc and any other callbacks |
| // to avoid further problems. |
| g_module_unloaded = true; |
| Win32RendererClient::OnModuleUnloaded(); |
| } |
| |
| } // namespace win32 |
| } // namespace mozc |