/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef IMMHandler_h_ #define IMMHandler_h_ #include "mozilla/ContentData.h" #include "mozilla/EventForwards.h" #include "mozilla/TextEventDispatcher.h" #include "mozilla/WritingModes.h" #include "windef.h" #include "winnetwk.h" #include "npapi.h" #include "nsCOMPtr.h" #include "nsIWidget.h" #include "nsRect.h" #include "nsString.h" #include "nsTArray.h" #include class nsWindow; namespace mozilla { namespace widget { struct MSGResult; class IMEContext final { public: IMEContext() : mWnd(nullptr), mIMC(nullptr) {} explicit IMEContext(HWND aWnd); explicit IMEContext(nsWindow* aWindowBase); ~IMEContext() { Clear(); } HIMC get() const { return mIMC; } void Init(HWND aWnd); void Init(nsWindow* aWindowBase); void Clear(); bool IsValid() const { return !!mIMC; } void SetOpenState(bool aOpen) const { if (!mIMC) { return; } ::ImmSetOpenStatus(mIMC, aOpen); } bool GetOpenState() const { if (!mIMC) { return false; } return (::ImmGetOpenStatus(mIMC) != FALSE); } bool AssociateDefaultContext() { // We assume that there is only default IMC, no new IMC has been created. if (mIMC) { return false; } if (!::ImmAssociateContextEx(mWnd, nullptr, IACE_DEFAULT)) { return false; } mIMC = ::ImmGetContext(mWnd); return (mIMC != nullptr); } bool Disassociate() { if (!mIMC) { return false; } if (!::ImmAssociateContextEx(mWnd, nullptr, 0)) { return false; } ::ImmReleaseContext(mWnd, mIMC); mIMC = nullptr; return true; } protected: IMEContext(const IMEContext& aOther) { MOZ_CRASH("Don't copy IMEContext"); } HWND mWnd; HIMC mIMC; }; class IMMHandler final { public: static void Initialize(); static void Terminate(); // If Process*() returns true, the caller shouldn't do anything anymore. static bool ProcessMessage(nsWindow* aWindow, UINT msg, WPARAM& wParam, LPARAM& lParam, MSGResult& aResult); static bool IsComposing() { return IsComposingOnOurEditor(); } static bool IsComposingOn(nsWindow* aWindow) { return IsComposing() && IsComposingWindow(aWindow); } #ifdef DEBUG /** * IsIMEAvailable() returns TRUE when current keyboard layout has IME. * Otherwise, FALSE. */ static bool IsIMEAvailable() { return !!::ImmIsIME(::GetKeyboardLayout(0)); } #endif // If aForce is TRUE, these methods doesn't check whether we have composition // or not. If you don't set it to TRUE, these method doesn't commit/cancel // the composition on uexpected window. static void CommitComposition(nsWindow* aWindow, bool aForce = false); static void CancelComposition(nsWindow* aWindow, bool aForce = false); static void OnFocusChange(bool aFocus, nsWindow* aWindow); static void OnUpdateComposition(nsWindow* aWindow); static void OnSelectionChange(nsWindow* aWindow, const IMENotification& aIMENotification, bool aIsIMMActive); static IMENotificationRequests GetIMENotificationRequests(); // Returns NS_SUCCESS_EVENT_CONSUMED if the mouse button event is consumed by // IME. Otherwise, NS_OK. static nsresult OnMouseButtonEvent(nsWindow* aWindow, const IMENotification& aIMENotification); #define DECL_IS_IME_ACTIVE(aReadableName) \ static bool Is##aReadableName##Active(); // Japanese IMEs DECL_IS_IME_ACTIVE(ATOK2006) DECL_IS_IME_ACTIVE(ATOK2007) DECL_IS_IME_ACTIVE(ATOK2008) DECL_IS_IME_ACTIVE(ATOK2009) DECL_IS_IME_ACTIVE(ATOK2010) DECL_IS_IME_ACTIVE(GoogleJapaneseInput) DECL_IS_IME_ACTIVE(Japanist2003) #undef DECL_IS_IME_ACTIVE /** * IsActiveIMEInBlockList() returns true if we know active keyboard layout's * IME has some crash bugs or something which make some damage to us. When * this returns true, IMC shouldn't be associated with any windows. */ static bool IsActiveIMEInBlockList(); protected: static void EnsureHandlerInstance(); static bool IsComposingOnOurEditor(); static bool IsComposingWindow(nsWindow* aWindow); static bool ShouldDrawCompositionStringOurselves(); static bool IsVerticalWritingSupported(); // aWindow can be nullptr if it's called without receiving WM_INPUTLANGCHANGE. static void InitKeyboardLayout(nsWindow* aWindow, HKL aKeyboardLayout); static UINT GetKeyboardCodePage(); /** * Checks whether the window is top level window of the composing window. * In this method, the top level window means in all windows, not only in all * OUR windows. I.e., if the aWindow is embedded, this always returns FALSE. */ static bool IsTopLevelWindowOfComposition(nsWindow* aWindow); static bool ProcessInputLangChangeMessage(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, MSGResult& aResult); IMMHandler(); ~IMMHandler(); // On*() methods return true if the caller of message handler shouldn't do // anything anymore. Otherwise, false. static bool OnKeyDownEvent(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, MSGResult& aResult); bool OnIMEStartComposition(nsWindow* aWindow, MSGResult& aResult); bool OnIMEComposition(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, MSGResult& aResult); bool OnIMEEndComposition(nsWindow* aWindow, MSGResult& aResult); bool OnIMERequest(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, MSGResult& aResult); bool OnChar(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, MSGResult& aResult); void OnInputLangChange(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, MSGResult& aResult); // These message handlers don't use instance members, we should not create // the instance by the messages. So, they should be static. static bool OnIMEChar(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, MSGResult& aResult); static bool OnIMESetContext(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, MSGResult& aResult); static bool OnIMECompositionFull(nsWindow* aWindow, MSGResult& aResult); static bool OnIMENotify(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, MSGResult& aResult); static bool OnIMESelect(nsWindow* aWindow, WPARAM wParam, LPARAM lParam, MSGResult& aResult); // The result of Handle* method mean "Processed" when it's TRUE. void HandleStartComposition(nsWindow* aWindow, const IMEContext& aContext); bool HandleComposition(nsWindow* aWindow, const IMEContext& aContext, LPARAM lParam); // If aCommitString is null, this commits composition with the latest // dispatched data. Otherwise, commits composition with the value. void HandleEndComposition(nsWindow* aWindow, const nsAString* aCommitString = nullptr); bool HandleReconvert(nsWindow* aWindow, LPARAM lParam, LRESULT* oResult); bool HandleQueryCharPosition(nsWindow* aWindow, LPARAM lParam, LRESULT* oResult); bool HandleDocumentFeed(nsWindow* aWindow, LPARAM lParam, LRESULT* oResult); /** * When a window's IME context is activating but we have composition on * another window, we should commit our composition because IME context is * shared by all our windows (including plug-ins). * @param aWindow is a new activated window. * If aWindow is our composing window, this method does nothing. * Otherwise, this commits the composition on the previous window. * If this method did commit a composition, this returns TRUE. */ bool CommitCompositionOnPreviousWindow(nsWindow* aWindow); /** * ResolveIMECaretPos * Convert the caret rect of a composition event to another widget's * coordinate system. * * @param aReferenceWidget The origin widget of aCursorRect. * Typically, this is mReferenceWidget of the * composing events. If the aCursorRect is in screen * coordinates, set nullptr. * @param aCursorRect The cursor rect. * @param aNewOriginWidget aOutRect will be in this widget's coordinates. If * this is nullptr, aOutRect will be in screen * coordinates. * @param aOutRect The converted cursor rect. */ void ResolveIMECaretPos(nsIWidget* aReferenceWidget, mozilla::LayoutDeviceIntRect& aCursorRect, nsIWidget* aNewOriginWidget, mozilla::LayoutDeviceIntRect& aOutRect); bool ConvertToANSIString(const nsString& aStr, UINT aCodePage, nsACString& aANSIStr); bool SetIMERelatedWindowsPos(nsWindow* aWindow, const IMEContext& aContext); /** * GetCharacterRectOfSelectedTextAt() returns character rect of the offset * from the selection start or the start of composition string if there is * a composition. * * @param aWindow The window which has focus. * @param aOffset Offset from the selection start or the start of * composition string when there is a composition. * This must be in the selection range or * the composition string. * @param aCharRect The result. * @param aWritingMode The writing mode of current selection. When this * is nullptr, this assumes that the selection is in * horizontal writing mode. * @return true if this succeeded to retrieve the rect. * Otherwise, false. */ bool GetCharacterRectOfSelectedTextAt( nsWindow* aWindow, uint32_t aOffset, mozilla::LayoutDeviceIntRect& aCharRect, mozilla::WritingMode* aWritingMode = nullptr); /** * GetCaretRect() returns caret rect at current selection start. * * @param aWindow The window which has focus. * @param aCaretRect The result. * @param aWritingMode The writing mode of current selection. When this * is nullptr, this assumes that the selection is in * horizontal writing mode. * @return true if this succeeded to retrieve the rect. * Otherwise, false. */ bool GetCaretRect(nsWindow* aWindow, mozilla::LayoutDeviceIntRect& aCaretRect, mozilla::WritingMode* aWritingMode = nullptr); void GetCompositionString(const IMEContext& aContext, DWORD aIndex, nsAString& aCompositionString) const; /** * AdjustCompositionFont() makes IME vertical writing mode if it's supported. * If aForceUpdate is true, it will update composition font even if writing * mode isn't being changed. */ void AdjustCompositionFont(nsWindow* aWindow, const IMEContext& aContext, const mozilla::WritingMode& aWritingMode, bool aForceUpdate = false); /** * MaybeAdjustCompositionFont() calls AdjustCompositionFont() when the * locale of active IME is CJK. Note that this creates an instance even * when there is no composition but the locale is CJK. */ static void MaybeAdjustCompositionFont( nsWindow* aWindow, const mozilla::WritingMode& aWritingMode, bool aForceUpdate = false); /** * Get the current target clause of composition string. * If there are one or more characters whose attribute is ATTR_TARGET_*, * this returns the first character's offset and its length. * Otherwise, e.g., the all characters are ATTR_INPUT, this returns * the composition string range because the all is the current target. * * aLength can be null (default), but aOffset must not be null. * * The aOffset value is offset in the contents. So, when you need offset * in the composition string, you need to subtract mCompositionStart from it. */ bool GetTargetClauseRange(uint32_t* aOffset, uint32_t* aLength = nullptr); /** * DispatchEvent() dispatches aEvent if aWidget hasn't been destroyed yet. */ static void DispatchEvent(nsWindow* aWindow, WidgetGUIEvent& aEvent); /** * DispatchCompositionChangeEvent() dispatches eCompositionChange event * with clause information (it'll be retrieved by CreateTextRangeArray()). * I.e., this should be called only during composing. If a composition is * being committed, only HandleCompositionEnd() should be called. * * @param aWindow The window which has the composition. * @param aContext Native IME context which has the composition. */ void DispatchCompositionChangeEvent(nsWindow* aWindow, const IMEContext& aContext); nsresult EnsureClauseArray(int32_t aCount); nsresult EnsureAttributeArray(int32_t aCount); /** * When WM_IME_CHAR is received and passed to DefWindowProc, we need to * record the messages. In other words, we should record the messages * when we receive WM_IME_CHAR on windowless plug-in (if we have focus, * we always eat them). When focus is moved from a windowless plug-in to * our window during composition, WM_IME_CHAR messages were received when * the plug-in has focus. However, WM_CHAR messages are received after the * plug-in lost focus. So, we need to ignore the WM_CHAR messages because * they make unexpected text input events on us. */ nsTArray mPassedIMEChar; bool IsIMECharRecordsEmpty() { return mPassedIMEChar.IsEmpty(); } void ResetIMECharRecords() { mPassedIMEChar.Clear(); } void DequeueIMECharRecords(WPARAM& wParam, LPARAM& lParam) { MSG msg = mPassedIMEChar.ElementAt(0); wParam = msg.wParam; lParam = msg.lParam; mPassedIMEChar.RemoveElementAt(0); } void EnqueueIMECharRecords(WPARAM wParam, LPARAM lParam) { MSG msg; msg.wParam = wParam; msg.lParam = lParam; mPassedIMEChar.AppendElement(msg); } TextEventDispatcher* GetTextEventDispatcherFor(nsWindow* aWindow); nsWindow* mComposingWindow; RefPtr mDispatcher; nsString mCompositionString; nsTArray mClauseArray; nsTArray mAttributeArray; int32_t mCursorPosition; uint32_t mCompositionStart; // mContentSelection stores the latest selection data only when sHasFocus is // true. Don't access mContentSelection directly. You should use // GetContentSelectionWithQueryIfNothing() for getting proper state. Maybe mContentSelection; const Maybe& GetContentSelectionWithQueryIfNothing( nsWindow* aWindow) { // When IME has focus, mContentSelection is automatically updated by // NOTIFY_IME_OF_SELECTION_CHANGE. if (sHasFocus) { if (mContentSelection.isNothing()) { // But if this is the first access of mContentSelection, we need to // query selection now. mContentSelection = QueryContentSelection(aWindow); } return mContentSelection; } // Otherwise, i.e., While IME doesn't have focus, we cannot observe // selection changes. So, in such case, we need to query selection // when it's necessary. static Maybe sTempContentSelection; sTempContentSelection = QueryContentSelection(aWindow); return sTempContentSelection; } /** * Query content selection on aWindow with WidgetQueryContent event. */ static Maybe QueryContentSelection(nsWindow* aWindow); bool mIsComposing; static mozilla::WritingMode sWritingModeOfCompositionFont; static nsString sIMEName; static UINT sCodePage; static DWORD sIMEProperty; static DWORD sIMEUIProperty; static bool sAssumeVerticalWritingModeNotSupported; static bool sHasFocus; }; } // namespace widget } // namespace mozilla #endif // IMMHandler_h_