Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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
#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 <windows.h>
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<MSG> 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<TextEventDispatcher> mDispatcher;
nsString mCompositionString;
nsTArray<uint32_t> mClauseArray;
nsTArray<uint8_t> 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<ContentSelection> mContentSelection;
const Maybe<ContentSelection>& 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<ContentSelection> sTempContentSelection;
sTempContentSelection = QueryContentSelection(aWindow);
return sTempContentSelection;
}
/**
* Query content selection on aWindow with WidgetQueryContent event.
*/
static Maybe<ContentSelection> 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_