| // 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/tip/tip_ui_element_delegate.h" |
| |
| #define _ATL_NO_AUTOMATIC_NAMESPACE |
| #define _WTL_NO_AUTOMATIC_NAMESPACE |
| #include <atlbase.h> |
| #include <atlcom.h> |
| #include <atlstr.h> |
| #include <msctf.h> |
| |
| #include <string> |
| |
| #include "base/util.h" |
| #include "renderer/renderer_command.pb.h" |
| #include "session/commands.pb.h" |
| #include "win32/base/conversion_mode_util.h" |
| #include "win32/base/input_state.h" |
| #include "win32/tip/tip_dll_module.h" |
| #include "win32/tip/tip_edit_session.h" |
| #include "win32/tip/tip_private_context.h" |
| #include "win32/tip/tip_resource.h" |
| #include "win32/tip/tip_text_service.h" |
| |
| namespace mozc { |
| namespace win32 { |
| namespace tsf { |
| |
| namespace { |
| |
| using ATL::CComBSTR; |
| using ATL::CComPtr; |
| using ATL::CComQIPtr; |
| using ATL::CComVariant; |
| using ATL::CStringW; |
| using mozc::commands::CompositionMode; |
| using ::mozc::commands::CandidateList; |
| using ::mozc::commands::Output; |
| using ::mozc::commands::Status; |
| typedef mozc::commands::RendererCommand_IndicatorInfo IndicatorInfo; |
| |
| const size_t kPageSize = 9; |
| |
| // This GUID is used in Windows Vista/7/8 by MS-IME to represents if the |
| // candidate window is visible or not. |
| // TODO(yukawa): Make sure if it is safe to use this GUID. |
| // {B7A578D2-9332-438A-A403-4057D05C3958} |
| const GUID kGuidCUASCandidateMessageCompartment = { |
| 0xb7a578d2, 0x9332, 0x438a, {0xa4, 0x03, 0x40, 0x57, 0xd0, 0x5c, 0x39, 0x58} |
| }; |
| |
| #ifdef GOOGLE_JAPANESE_INPUT_BUILD |
| |
| // {8F51B5E5-5CF9-45D8-83B3-53CE203354C2} |
| const GUID KGuidNonobservableSuggestWindow = { |
| 0x8f51b5e5, 0x5cf9, 0x45d8, {0x83, 0xb3, 0x53, 0xce, 0x20, 0x33, 0x54, 0xc2} |
| }; |
| |
| // {3D53878A-8596-4689-B50D-3338D52B2EFB} |
| const GUID KGuidObservableSuggestWindow = { |
| 0x3d53878a, 0x8596, 0x4689, {0xb5, 0xd, 0x33, 0x38, 0xd5, 0x2b, 0x2e, 0xfb} |
| }; |
| |
| // {FED897F2-940C-40F1-B149-A931E03FB821} |
| const GUID KGuidCandidateWindow = { |
| 0xfed897f2, 0x940c, 0x40f1, {0xb1, 0x49, 0xa9, 0x31, 0xe0, 0x3f, 0xb8, 0x21} |
| }; |
| |
| // {170F6CC4-913D-4FF9-9DEA-432D08DCB0FF} |
| const GUID KGuidIndicatorWindow = { |
| 0x170f6cc4, 0x913d, 0x4ff9, { 0x9d, 0xea, 0x43, 0x2d, 0x8, 0xdc, 0xb0, 0xff} |
| }; |
| |
| #else |
| |
| // {AD2489FB-D4C4-4632-85A9-7F9F917AB0FD} |
| const GUID KGuidNonobservableSuggestWindow = { |
| 0xad2489fb, 0xd4c4, 0x4632, {0x85, 0xa9, 0x7f, 0x9f, 0x91, 0x7a, 0xb0, 0xfd} |
| }; |
| |
| // {0E2D447F-9B4A-490C-9C4D-61A6A707BE26} |
| const GUID KGuidObservableSuggestWindow = { |
| 0xe2d447f, 0x9b4a, 0x490c, {0x9c, 0x4d, 0x61, 0xa6, 0xa7, 0x7, 0xbe, 0x26} |
| }; |
| |
| // {ED70ECDE-C8AA-4170-96CC-0090DEA8AEC2} |
| const GUID KGuidCandidateWindow = { |
| 0xed70ecde, 0xc8aa, 0x4170, {0x96, 0xcc, 0x0, 0x90, 0xde, 0xa8, 0xae, 0xc2} |
| }; |
| |
| // {0090BF80-5F33-41B1-843C-E3EC79ED25F9} |
| const GUID KGuidIndicatorWindow = { |
| 0x90bf80, 0x5f33, 0x41b1, { 0x84, 0x3c, 0xe3, 0xec, 0x79, 0xed, 0x25, 0xf9} |
| }; |
| |
| #endif // GOOGLE_JAPANESE_INPUT_BUILD |
| |
| CComBSTR GetResourceString(UINT resource_id) { |
| CStringW str; |
| str.LoadStringW(TipDllModule::module_handle(), resource_id); |
| return CComBSTR(str.GetLength(), str.GetBuffer()); |
| } |
| |
| const bool kIsIndicator = true; |
| const bool kIsNotIndicator = false; |
| |
| class TipUiElementDelegateImpl : public TipUiElementDelegate { |
| public: |
| TipUiElementDelegateImpl( |
| TipTextService *text_service, ITfContext *context, |
| TipUiElementDelegateFactory::ElementType type) |
| : text_service_(text_service), |
| context_(context), |
| type_(type) { |
| } |
| |
| private: |
| ~TipUiElementDelegateImpl() {} |
| |
| virtual bool IsObservable() const { |
| switch (type_) { |
| case TipUiElementDelegateFactory::kConventionalObservableSuggestWindow: |
| case TipUiElementDelegateFactory::kConventionalCandidateWindow: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| // The ITfUIElement interface methods |
| virtual HRESULT GetDescription(BSTR *description) { |
| if (description == nullptr) { |
| return E_INVALIDARG; |
| } |
| *description = nullptr; |
| switch (type_) { |
| case TipUiElementDelegateFactory::kConventionalUnobservableSuggestWindow: |
| *description = |
| GetResourceString(IDS_UNOBSERVABLE_SUGGEST_WINDOW).Detach(); |
| return S_OK; |
| case TipUiElementDelegateFactory::kConventionalObservableSuggestWindow: |
| *description = |
| GetResourceString(IDS_OBSERVABLE_SUGGEST_WINDOW).Detach(); |
| return S_OK; |
| case TipUiElementDelegateFactory::kConventionalCandidateWindow: |
| *description = GetResourceString(IDS_CANDIDATE_WINDOW).Detach(); |
| return S_OK; |
| case TipUiElementDelegateFactory::kConventionalIndicatorWindow: |
| *description = GetResourceString(IDS_INDICATOR_WINDOW).Detach(); |
| return S_OK; |
| case TipUiElementDelegateFactory::kImmersiveCandidateWindow: |
| *description = GetResourceString(IDS_CANDIDATE_WINDOW).Detach(); |
| return S_OK; |
| case TipUiElementDelegateFactory::kImmersiveIndicatorWindow: |
| *description = GetResourceString(IDS_INDICATOR_WINDOW).Detach(); |
| return S_OK; |
| default: |
| return E_UNEXPECTED; |
| } |
| } |
| |
| virtual HRESULT GetGUID(GUID *guid) { |
| if (guid == nullptr) { |
| return E_INVALIDARG; |
| } |
| switch (type_) { |
| case TipUiElementDelegateFactory::kConventionalUnobservableSuggestWindow: |
| *guid = KGuidNonobservableSuggestWindow; |
| return S_OK; |
| case TipUiElementDelegateFactory::kConventionalObservableSuggestWindow: |
| *guid = KGuidObservableSuggestWindow; |
| return S_OK; |
| case TipUiElementDelegateFactory::kConventionalCandidateWindow: |
| *guid = KGuidCandidateWindow; |
| return S_OK; |
| case TipUiElementDelegateFactory::kConventionalIndicatorWindow: |
| *guid = KGuidIndicatorWindow; |
| return S_OK; |
| case TipUiElementDelegateFactory::kImmersiveCandidateWindow: |
| *guid = KGuidCandidateWindow; |
| return S_OK; |
| case TipUiElementDelegateFactory::kImmersiveIndicatorWindow: |
| *guid = KGuidIndicatorWindow; |
| return S_OK; |
| default: |
| *guid = GUID_NULL; |
| return E_UNEXPECTED; |
| } |
| } |
| |
| virtual HRESULT Show(BOOL show) { |
| const bool old_shown = shown_; |
| shown_ = !!show; |
| if (old_shown != shown_ && IsObservable()) { |
| CComQIPtr<ITfCompartmentMgr> compartment_mgr = context_; |
| if (compartment_mgr) { |
| // Update a hidden compartment to generate |
| // IMN_OPENCANDIDATE/IMN_CLOSECANDIDATE notifications for the |
| // application compatibility. |
| CComQIPtr<ITfCompartment> compartment; |
| if (SUCCEEDED(compartment_mgr->GetCompartment( |
| kGuidCUASCandidateMessageCompartment, &compartment))) { |
| CComVariant var; |
| var.vt = VT_I4; |
| var.lVal = shown_ ? TRUE : FALSE; |
| compartment->SetValue(text_service_->GetClientID(), &var); |
| } |
| } |
| // TODO(yukawa): Update UI. |
| } |
| return S_OK; |
| } |
| |
| virtual HRESULT IsShown(BOOL *show) { |
| if (show == nullptr) { |
| return E_INVALIDARG; |
| } |
| *show = (shown_ ? TRUE : FALSE); |
| return S_OK; |
| } |
| |
| // The ITfCandidateListUIElement interface methods |
| virtual HRESULT GetUpdatedFlags(DWORD *flags) { |
| DCHECK(IsCandidateWindowLike()); |
| |
| if (flags == nullptr) { |
| return E_INVALIDARG; |
| } |
| *flags = 0; |
| // If TF_CLUIE_STRING is included into |flags|, TSF calls back |
| // ITfCandidateListUIElement::GetString for all the candidates, |
| // which might be a huge bottleneck. So do not include this flag |
| // whenever possible. |
| if (TestModifiedAndUpdateLastCandidate()) { |
| *flags |= (TF_CLUIE_STRING | TF_CLUIE_COUNT); |
| } |
| *flags |= (TF_CLUIE_SELECTION | |
| TF_CLUIE_CURRENTPAGE | |
| TF_CLUIE_PAGEINDEX); |
| return S_OK; |
| } |
| |
| virtual HRESULT GetDocumentMgr(ITfDocumentMgr **document_manager) { |
| DCHECK(IsCandidateWindowLike()); |
| |
| if (document_manager == nullptr) { |
| return E_INVALIDARG; |
| } |
| return context_->GetDocumentMgr(document_manager); |
| } |
| |
| virtual HRESULT GetCount(UINT *count) { |
| DCHECK(IsCandidateWindowLike()); |
| |
| if (count == nullptr) { |
| return E_INVALIDARG; |
| } |
| *count = 0; |
| TipPrivateContext *private_context = |
| text_service_->GetPrivateContext(context_); |
| if (private_context == nullptr) { |
| return E_FAIL; |
| } |
| const Output &output = private_context->last_output(); |
| if (!output.has_all_candidate_words()) { |
| return S_OK; |
| } |
| *count = output.all_candidate_words().candidates_size(); |
| return S_OK; |
| } |
| |
| virtual HRESULT GetSelection(UINT *index) { |
| DCHECK(IsCandidateWindowLike()); |
| |
| if (index == nullptr) { |
| return E_INVALIDARG; |
| } |
| *index = 0; |
| TipPrivateContext *private_context = |
| text_service_->GetPrivateContext(context_); |
| if (private_context == nullptr) { |
| return E_FAIL; |
| } |
| const Output &output = private_context->last_output(); |
| if (!output.has_all_candidate_words()) { |
| return S_OK; |
| } |
| *index = output.all_candidate_words().focused_index(); |
| return S_OK; |
| } |
| |
| virtual HRESULT GetString(UINT index, BSTR *text) { |
| DCHECK(IsCandidateWindowLike()); |
| |
| if (text == nullptr) { |
| return E_INVALIDARG; |
| } |
| *text = nullptr; |
| TipPrivateContext *private_context = |
| text_service_->GetPrivateContext(context_); |
| if (private_context == nullptr) { |
| return E_FAIL; |
| } |
| const Output &output = private_context->last_output(); |
| if (!output.has_all_candidate_words()) { |
| return E_FAIL; |
| } |
| const CandidateList &list = output.all_candidate_words(); |
| // Convert |index| to the index within output_->candidates(). |
| const int visible_index = index; |
| if (visible_index >= list.candidates_size()) { |
| return E_FAIL; |
| } |
| wstring wide_text; |
| Util::UTF8ToWide(list.candidates(visible_index).value(), &wide_text); |
| *text = CComBSTR(wide_text.size(), wide_text.data()).Detach(); |
| return S_OK; |
| } |
| |
| virtual HRESULT GetPageIndex(UINT *index, UINT size, UINT *page_count) { |
| DCHECK(IsCandidateWindowLike()); |
| |
| if (page_count == nullptr) { |
| return E_INVALIDARG; |
| } |
| TipPrivateContext *private_context = |
| text_service_->GetPrivateContext(context_); |
| if (private_context == nullptr) { |
| return E_FAIL; |
| } |
| const Output &output = private_context->last_output(); |
| if (!output.has_all_candidate_words()) { |
| return E_FAIL; |
| } |
| const CandidateList &list = output.all_candidate_words(); |
| const size_t max_page = (list.candidates_size() / kPageSize); |
| *page_count = max_page + 1; |
| |
| if (index == nullptr) { |
| // An application can pass nullptr as |index| to obtain only page_count. |
| return S_OK; |
| } |
| |
| if (size < *page_count) { |
| return E_NOT_SUFFICIENT_BUFFER; |
| } |
| for (size_t i = 0; i < *page_count; ++i) { |
| index[i] = i * kPageSize; |
| } |
| return S_OK; |
| } |
| |
| virtual HRESULT SetPageIndex(UINT *index, UINT page_count) { |
| DCHECK(IsCandidateWindowLike()); |
| |
| return E_NOTIMPL; |
| } |
| |
| virtual HRESULT GetCurrentPage(UINT *current_page) { |
| DCHECK(IsCandidateWindowLike()); |
| |
| if (current_page == nullptr) { |
| return E_INVALIDARG; |
| } |
| *current_page = 0; |
| TipPrivateContext *private_context = |
| text_service_->GetPrivateContext(context_); |
| if (private_context == nullptr) { |
| return E_FAIL; |
| } |
| const Output &output = private_context->last_output(); |
| if (!output.has_all_candidate_words()) { |
| return S_OK; |
| } |
| *current_page = output.all_candidate_words().focused_index() / kPageSize; |
| return S_OK; |
| } |
| |
| // The ITfCandidateListUIElementBehavior interface methods |
| virtual HRESULT SetSelection(UINT index) { |
| DCHECK(IsCandidateWindowLike()); |
| |
| TipPrivateContext *private_context = |
| text_service_->GetPrivateContext(context_); |
| if (private_context == nullptr) { |
| return E_FAIL; |
| } |
| const Output &output = private_context->last_output(); |
| if (!output.has_all_candidate_words()) { |
| return E_FAIL; |
| } |
| const CandidateList &list = output.all_candidate_words(); |
| if (list.candidates_size() <= index) { |
| return E_INVALIDARG; |
| } |
| const int id = list.candidates(index).id(); |
| if (!TipEditSession::SelectCandidateAsync(text_service_, context_, id)) { |
| return E_FAIL; |
| } |
| return S_OK; |
| } |
| |
| virtual HRESULT Finalize() { |
| DCHECK(IsCandidateWindowLike()); |
| |
| if (!TipEditSession::SubmitAsync(text_service_, context_)) { |
| return E_FAIL; |
| } |
| return S_OK; |
| } |
| |
| virtual HRESULT Abort() { |
| DCHECK(IsCandidateWindowLike()); |
| |
| // Currently equals to Finalize(). |
| if (!TipEditSession::SubmitAsync(text_service_, context_)) { |
| return E_FAIL; |
| } |
| return S_OK; |
| } |
| |
| virtual HRESULT GetString(BSTR *text) { |
| DCHECK(IsIndicator()); |
| |
| if (text == nullptr) { |
| return E_INVALIDARG; |
| } |
| |
| CStringW msg = L""; |
| |
| TipPrivateContext *private_context = |
| text_service_->GetPrivateContext(context_); |
| if (private_context == nullptr) { |
| *text = CComBSTR(msg.GetLength(), msg.GetBuffer()).Detach(); |
| return S_OK; |
| } |
| if (!private_context->last_output().has_status()) { |
| *text = CComBSTR(msg.GetLength(), msg.GetBuffer()).Detach(); |
| return S_OK; |
| } |
| const Status &status = private_context->last_output().status(); |
| if (status.has_activated() && !status.activated()) { |
| msg = L"A"; |
| *text = CComBSTR(msg.GetLength(), msg.GetBuffer()).Detach(); |
| return S_OK; |
| } |
| if (!status.has_mode()) { |
| *text = CComBSTR(msg.GetLength(), msg.GetBuffer()).Detach(); |
| return S_OK; |
| } |
| |
| switch (status.mode()) { |
| case commands::DIRECT: |
| DLOG(FATAL) << "Must not reach here."; |
| break; |
| case commands::HIRAGANA: |
| msg = L"\u3042"; |
| break; |
| case commands::FULL_KATAKANA: |
| msg = L"\u30AB"; |
| break; |
| case commands::HALF_ASCII: |
| msg = L"_A"; |
| break; |
| case commands::FULL_ASCII: |
| msg = L"\uFF21"; |
| break; |
| case commands::HALF_KATAKANA: |
| msg = L"_\uFF76"; |
| break; |
| default: |
| break; |
| } |
| |
| *text = CComBSTR(msg.GetLength(), msg.GetBuffer()).Detach(); |
| return S_OK; |
| } |
| |
| // Returns true if the candidate list is updated. When this function returns |
| // false, you need not update the list of candidate strings at this time. |
| // Note that this function updates |last_candidate_list_| internally. |
| bool TestModifiedAndUpdateLastCandidate() { |
| TipPrivateContext *private_context = |
| text_service_->GetPrivateContext(context_); |
| if (private_context == nullptr) { |
| return true; |
| } |
| const Output &output = private_context->last_output(); |
| if (!output.has_all_candidate_words()) { |
| return true; |
| } |
| const CandidateList &list = output.all_candidate_words(); |
| if (last_candidate_list_.candidates_size() != list.candidates_size()) { |
| last_candidate_list_.CopyFrom(list); |
| return true; |
| } |
| for (int i = 0; i < list.candidates_size(); ++i) { |
| if (last_candidate_list_.candidates(i).value() != |
| list.candidates(i).value()) { |
| last_candidate_list_.CopyFrom(list); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| bool IsCandidateWindowLike() const { |
| switch (type_) { |
| case TipUiElementDelegateFactory::kConventionalUnobservableSuggestWindow: |
| return true; |
| case TipUiElementDelegateFactory::kConventionalObservableSuggestWindow: |
| return true; |
| case TipUiElementDelegateFactory::kConventionalCandidateWindow: |
| return true; |
| case TipUiElementDelegateFactory::kConventionalIndicatorWindow: |
| return false; |
| case TipUiElementDelegateFactory::kImmersiveCandidateWindow: |
| return true; |
| case TipUiElementDelegateFactory::kImmersiveIndicatorWindow: |
| return false; |
| default: |
| return false; |
| } |
| } |
| |
| bool IsIndicator() const { |
| switch (type_) { |
| case TipUiElementDelegateFactory::kConventionalUnobservableSuggestWindow: |
| return false; |
| case TipUiElementDelegateFactory::kConventionalObservableSuggestWindow: |
| return false; |
| case TipUiElementDelegateFactory::kConventionalCandidateWindow: |
| return false; |
| case TipUiElementDelegateFactory::kConventionalIndicatorWindow: |
| return true; |
| case TipUiElementDelegateFactory::kImmersiveCandidateWindow: |
| return false; |
| case TipUiElementDelegateFactory::kImmersiveIndicatorWindow: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| CComPtr<TipTextService> text_service_; |
| CComPtr<ITfContext> context_; |
| const TipUiElementDelegateFactory::ElementType type_; |
| CandidateList last_candidate_list_; |
| bool shown_; |
| DISALLOW_COPY_AND_ASSIGN(TipUiElementDelegateImpl); |
| }; |
| |
| } // namespace |
| |
| TipUiElementDelegate::TipUiElementDelegate() {} |
| TipUiElementDelegate::~TipUiElementDelegate() {} |
| |
| TipUiElementDelegate *TipUiElementDelegateFactory::Create( |
| TipTextService *text_service, ITfContext *context, ElementType type) { |
| return new TipUiElementDelegateImpl(text_service, context, type); |
| } |
| |
| } // namespace tsf |
| } // namespace win32 |
| } // namespace mozc |