summaryrefslogtreecommitdiffstats
path: root/widget/windows/TSFTextStore.h
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--widget/windows/TSFTextStore.h1053
1 files changed, 1053 insertions, 0 deletions
diff --git a/widget/windows/TSFTextStore.h b/widget/windows/TSFTextStore.h
new file mode 100644
index 0000000000..7be04df276
--- /dev/null
+++ b/widget/windows/TSFTextStore.h
@@ -0,0 +1,1053 @@
+/* -*- 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 TSFTextStore_h_
+#define TSFTextStore_h_
+
+#include "nsCOMPtr.h"
+#include "nsIWidget.h"
+#include "nsString.h"
+#include "nsWindowBase.h"
+
+#include "WinUtils.h"
+#include "WritingModes.h"
+
+#include "mozilla/Attributes.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/TextEventDispatcher.h"
+#include "mozilla/TextRange.h"
+#include "mozilla/WindowsVersion.h"
+#include "mozilla/widget/IMEData.h"
+
+#include <msctf.h>
+#include <textstor.h>
+
+// GUID_PROP_INPUTSCOPE is declared in inputscope.h using INIT_GUID.
+// With initguid.h, we get its instance instead of extern declaration.
+#ifdef INPUTSCOPE_INIT_GUID
+# include <initguid.h>
+#endif
+#ifdef TEXTATTRS_INIT_GUID
+# include <tsattrs.h>
+#endif
+#include <inputscope.h>
+
+// TSF InputScope, for earlier SDK 8
+#define IS_SEARCH static_cast<InputScope>(50)
+
+struct ITfThreadMgr;
+struct ITfDocumentMgr;
+struct ITfDisplayAttributeMgr;
+struct ITfCategoryMgr;
+class nsWindow;
+
+inline std::ostream& operator<<(std::ostream& aStream,
+ const TS_SELECTIONSTYLE& aSelectionStyle) {
+ const char* ase = "Unknown";
+ switch (aSelectionStyle.ase) {
+ case TS_AE_START:
+ ase = "TS_AE_START";
+ break;
+ case TS_AE_END:
+ ase = "TS_AE_END";
+ break;
+ case TS_AE_NONE:
+ ase = "TS_AE_NONE";
+ break;
+ }
+ aStream << "{ ase=" << ase << ", fInterimChar="
+ << (aSelectionStyle.fInterimChar ? "TRUE" : "FALSE") << " }";
+ return aStream;
+}
+
+inline std::ostream& operator<<(std::ostream& aStream,
+ const TS_SELECTION_ACP& aACP) {
+ aStream << "{ acpStart=" << aACP.acpStart << ", acpEnd=" << aACP.acpEnd
+ << ", style=" << mozilla::ToString(aACP.style).c_str() << " }";
+ return aStream;
+}
+
+namespace mozilla {
+namespace widget {
+
+class TSFStaticSink;
+struct MSGResult;
+
+/*
+ * Text Services Framework text store
+ */
+
+class TSFTextStore final : public ITextStoreACP,
+ public ITfContextOwnerCompositionSink,
+ public ITfMouseTrackerACP {
+ friend class TSFStaticSink;
+
+ private:
+ typedef IMENotification::SelectionChangeDataBase SelectionChangeDataBase;
+ typedef IMENotification::SelectionChangeData SelectionChangeData;
+ typedef IMENotification::TextChangeDataBase TextChangeDataBase;
+ typedef IMENotification::TextChangeData TextChangeData;
+
+ public: /*IUnknown*/
+ STDMETHODIMP QueryInterface(REFIID, void**);
+
+ NS_INLINE_DECL_IUNKNOWN_REFCOUNTING(TSFTextStore)
+
+ public: /*ITextStoreACP*/
+ STDMETHODIMP AdviseSink(REFIID, IUnknown*, DWORD);
+ STDMETHODIMP UnadviseSink(IUnknown*);
+ STDMETHODIMP RequestLock(DWORD, HRESULT*);
+ STDMETHODIMP GetStatus(TS_STATUS*);
+ STDMETHODIMP QueryInsert(LONG, LONG, ULONG, LONG*, LONG*);
+ STDMETHODIMP GetSelection(ULONG, ULONG, TS_SELECTION_ACP*, ULONG*);
+ STDMETHODIMP SetSelection(ULONG, const TS_SELECTION_ACP*);
+ STDMETHODIMP GetText(LONG, LONG, WCHAR*, ULONG, ULONG*, TS_RUNINFO*, ULONG,
+ ULONG*, LONG*);
+ STDMETHODIMP SetText(DWORD, LONG, LONG, const WCHAR*, ULONG, TS_TEXTCHANGE*);
+ STDMETHODIMP GetFormattedText(LONG, LONG, IDataObject**);
+ STDMETHODIMP GetEmbedded(LONG, REFGUID, REFIID, IUnknown**);
+ STDMETHODIMP QueryInsertEmbedded(const GUID*, const FORMATETC*, BOOL*);
+ STDMETHODIMP InsertEmbedded(DWORD, LONG, LONG, IDataObject*, TS_TEXTCHANGE*);
+ STDMETHODIMP RequestSupportedAttrs(DWORD, ULONG, const TS_ATTRID*);
+ STDMETHODIMP RequestAttrsAtPosition(LONG, ULONG, const TS_ATTRID*, DWORD);
+ STDMETHODIMP RequestAttrsTransitioningAtPosition(LONG, ULONG,
+ const TS_ATTRID*, DWORD);
+ STDMETHODIMP FindNextAttrTransition(LONG, LONG, ULONG, const TS_ATTRID*,
+ DWORD, LONG*, BOOL*, LONG*);
+ STDMETHODIMP RetrieveRequestedAttrs(ULONG, TS_ATTRVAL*, ULONG*);
+ STDMETHODIMP GetEndACP(LONG*);
+ STDMETHODIMP GetActiveView(TsViewCookie*);
+ STDMETHODIMP GetACPFromPoint(TsViewCookie, const POINT*, DWORD, LONG*);
+ STDMETHODIMP GetTextExt(TsViewCookie, LONG, LONG, RECT*, BOOL*);
+ STDMETHODIMP GetScreenExt(TsViewCookie, RECT*);
+ STDMETHODIMP GetWnd(TsViewCookie, HWND*);
+ STDMETHODIMP InsertTextAtSelection(DWORD, const WCHAR*, ULONG, LONG*, LONG*,
+ TS_TEXTCHANGE*);
+ STDMETHODIMP InsertEmbeddedAtSelection(DWORD, IDataObject*, LONG*, LONG*,
+ TS_TEXTCHANGE*);
+
+ public: /*ITfContextOwnerCompositionSink*/
+ STDMETHODIMP OnStartComposition(ITfCompositionView*, BOOL*);
+ STDMETHODIMP OnUpdateComposition(ITfCompositionView*, ITfRange*);
+ STDMETHODIMP OnEndComposition(ITfCompositionView*);
+
+ public: /*ITfMouseTrackerACP*/
+ STDMETHODIMP AdviseMouseSink(ITfRangeACP*, ITfMouseSink*, DWORD*);
+ STDMETHODIMP UnadviseMouseSink(DWORD);
+
+ public:
+ static void Initialize(void);
+ static void Terminate(void);
+
+ static bool ProcessRawKeyMessage(const MSG& aMsg);
+ static void ProcessMessage(nsWindowBase* aWindow, UINT aMessage,
+ WPARAM& aWParam, LPARAM& aLParam,
+ MSGResult& aResult);
+
+ static void SetIMEOpenState(bool);
+ static bool GetIMEOpenState(void);
+
+ static void CommitComposition(bool aDiscard) {
+ NS_ASSERTION(IsInTSFMode(), "Not in TSF mode, shouldn't be called");
+ if (!sEnabledTextStore) {
+ return;
+ }
+ RefPtr<TSFTextStore> textStore(sEnabledTextStore);
+ textStore->CommitCompositionInternal(aDiscard);
+ }
+
+ static void SetInputContext(nsWindowBase* aWidget,
+ const InputContext& aContext,
+ const InputContextAction& aAction);
+
+ static nsresult OnFocusChange(bool aGotFocus, nsWindowBase* aFocusedWidget,
+ const InputContext& aContext);
+ static nsresult OnTextChange(const IMENotification& aIMENotification) {
+ NS_ASSERTION(IsInTSFMode(), "Not in TSF mode, shouldn't be called");
+ if (!sEnabledTextStore) {
+ return NS_OK;
+ }
+ RefPtr<TSFTextStore> textStore(sEnabledTextStore);
+ return textStore->OnTextChangeInternal(aIMENotification);
+ }
+
+ static nsresult OnSelectionChange(const IMENotification& aIMENotification) {
+ NS_ASSERTION(IsInTSFMode(), "Not in TSF mode, shouldn't be called");
+ if (!sEnabledTextStore) {
+ return NS_OK;
+ }
+ RefPtr<TSFTextStore> textStore(sEnabledTextStore);
+ return textStore->OnSelectionChangeInternal(aIMENotification);
+ }
+
+ static nsresult OnLayoutChange() {
+ NS_ASSERTION(IsInTSFMode(), "Not in TSF mode, shouldn't be called");
+ if (!sEnabledTextStore) {
+ return NS_OK;
+ }
+ RefPtr<TSFTextStore> textStore(sEnabledTextStore);
+ return textStore->OnLayoutChangeInternal();
+ }
+
+ static nsresult OnUpdateComposition() {
+ NS_ASSERTION(IsInTSFMode(), "Not in TSF mode, shouldn't be called");
+ if (!sEnabledTextStore) {
+ return NS_OK;
+ }
+ RefPtr<TSFTextStore> textStore(sEnabledTextStore);
+ return textStore->OnUpdateCompositionInternal();
+ }
+
+ static nsresult OnMouseButtonEvent(const IMENotification& aIMENotification) {
+ NS_ASSERTION(IsInTSFMode(), "Not in TSF mode, shouldn't be called");
+ if (!sEnabledTextStore) {
+ return NS_OK;
+ }
+ RefPtr<TSFTextStore> textStore(sEnabledTextStore);
+ return textStore->OnMouseButtonEventInternal(aIMENotification);
+ }
+
+ static IMENotificationRequests GetIMENotificationRequests();
+
+ // Returns the address of the pointer so that the TSF automatic test can
+ // replace the system object with a custom implementation for testing.
+ // XXX TSF doesn't work now. Should we remove it?
+ static void* GetNativeData(uint32_t aDataType) {
+ switch (aDataType) {
+ case NS_NATIVE_TSF_THREAD_MGR:
+ Initialize(); // Apply any previous changes
+ return static_cast<void*>(&sThreadMgr);
+ case NS_NATIVE_TSF_CATEGORY_MGR:
+ return static_cast<void*>(&sCategoryMgr);
+ case NS_NATIVE_TSF_DISPLAY_ATTR_MGR:
+ return static_cast<void*>(&sDisplayAttrMgr);
+ default:
+ return nullptr;
+ }
+ }
+
+ static void* GetThreadManager() { return static_cast<void*>(sThreadMgr); }
+
+ static bool ThinksHavingFocus() {
+ return (sEnabledTextStore && sEnabledTextStore->mContext);
+ }
+
+ static bool IsInTSFMode() { return sThreadMgr != nullptr; }
+
+ static bool IsComposing() {
+ return (sEnabledTextStore && sEnabledTextStore->mComposition.isSome());
+ }
+
+ static bool IsComposingOn(nsWindowBase* aWidget) {
+ return (IsComposing() && sEnabledTextStore->mWidget == aWidget);
+ }
+
+ static nsWindowBase* GetEnabledWindowBase() {
+ return sEnabledTextStore ? sEnabledTextStore->mWidget.get() : nullptr;
+ }
+
+ /**
+ * Returns true if active keyboard layout is a legacy IMM-IME.
+ */
+ static bool IsIMM_IMEActive();
+
+ /**
+ * Returns true if active TIP is MS-IME for Japanese.
+ */
+ static bool IsMSJapaneseIMEActive();
+
+ /**
+ * Returns true if active TIP is Google Japanese Input.
+ * Note that if Google Japanese Input is installed as an IMM-IME,
+ * this return false even if Google Japanese Input is active.
+ * So, you may need to check IMMHandler::IsGoogleJapaneseInputActive() too.
+ */
+ static bool IsGoogleJapaneseInputActive();
+
+ /**
+ * Returns true if active TIP or IME is a black listed one and we should
+ * set input scope of URL bar to IS_DEFAULT rather than IS_URL.
+ */
+ static bool ShouldSetInputScopeOfURLBarToDefault();
+
+ /**
+ * Returns true if TSF may crash if GetSelection() returns E_FAIL.
+ */
+ static bool DoNotReturnErrorFromGetSelection();
+
+#ifdef DEBUG
+ // Returns true when keyboard layout has IME (TIP).
+ static bool CurrentKeyboardLayoutHasIME();
+#endif // #ifdef DEBUG
+
+ protected:
+ TSFTextStore();
+ ~TSFTextStore();
+
+ static bool CreateAndSetFocus(nsWindowBase* aFocusedWidget,
+ const InputContext& aContext);
+ static void EnsureToDestroyAndReleaseEnabledTextStoreIf(
+ RefPtr<TSFTextStore>& aTextStore);
+ static void MarkContextAsKeyboardDisabled(ITfContext* aContext);
+ static void MarkContextAsEmpty(ITfContext* aContext);
+
+ bool Init(nsWindowBase* aWidget, const InputContext& aContext);
+ void Destroy();
+ void ReleaseTSFObjects();
+
+ bool IsReadLock(DWORD aLock) const {
+ return (TS_LF_READ == (aLock & TS_LF_READ));
+ }
+ bool IsReadWriteLock(DWORD aLock) const {
+ return (TS_LF_READWRITE == (aLock & TS_LF_READWRITE));
+ }
+ bool IsReadLocked() const { return IsReadLock(mLock); }
+ bool IsReadWriteLocked() const { return IsReadWriteLock(mLock); }
+
+ // This is called immediately after a call of OnLockGranted() of mSink.
+ // Note that mLock isn't cleared yet when this is called.
+ void DidLockGranted();
+
+ bool GetScreenExtInternal(RECT& aScreenExt);
+ // If aDispatchCompositionChangeEvent is true, this method will dispatch
+ // compositionchange event if this is called during IME composing.
+ // aDispatchCompositionChangeEvent should be true only when this is called
+ // from SetSelection. Because otherwise, the compositionchange event should
+ // not be sent from here.
+ HRESULT SetSelectionInternal(const TS_SELECTION_ACP*,
+ bool aDispatchCompositionChangeEvent = false);
+ bool InsertTextAtSelectionInternal(const nsAString& aInsertStr,
+ TS_TEXTCHANGE* aTextChange);
+ void CommitCompositionInternal(bool);
+ HRESULT GetDisplayAttribute(ITfProperty* aProperty, ITfRange* aRange,
+ TF_DISPLAYATTRIBUTE* aResult);
+ HRESULT RestartCompositionIfNecessary(ITfRange* pRangeNew = nullptr);
+ class Composition;
+ HRESULT RestartComposition(Composition& aCurrentComposition,
+ ITfCompositionView* aCompositionView,
+ ITfRange* aNewRange);
+
+ // Following methods record composing action(s) to mPendingActions.
+ // They will be flushed FlushPendingActions().
+ HRESULT RecordCompositionStartAction(ITfCompositionView* aCompositionView,
+ ITfRange* aRange,
+ bool aPreserveSelection);
+ HRESULT RecordCompositionStartAction(ITfCompositionView* aCompositionView,
+ LONG aStart, LONG aLength,
+ bool aPreserveSelection);
+ HRESULT RecordCompositionUpdateAction();
+ HRESULT RecordCompositionEndAction();
+
+ // DispatchEvent() dispatches the event and if it may not be handled
+ // synchronously, this makes the instance not notify TSF of pending
+ // notifications until next notification from content.
+ void DispatchEvent(WidgetGUIEvent& aEvent);
+ void OnLayoutInformationAvaliable();
+
+ // FlushPendingActions() performs pending actions recorded in mPendingActions
+ // and clear it.
+ void FlushPendingActions();
+ // MaybeFlushPendingNotifications() performs pending notifications to TSF.
+ void MaybeFlushPendingNotifications();
+
+ nsresult OnTextChangeInternal(const IMENotification& aIMENotification);
+ nsresult OnSelectionChangeInternal(const IMENotification& aIMENotification);
+ nsresult OnMouseButtonEventInternal(const IMENotification& aIMENotification);
+ nsresult OnLayoutChangeInternal();
+ nsresult OnUpdateCompositionInternal();
+
+ // mPendingSelectionChangeData stores selection change data until notifying
+ // TSF of selection change. If two or more selection changes occur, this
+ // stores the latest selection change data because only it is necessary.
+ SelectionChangeData mPendingSelectionChangeData;
+
+ // mPendingTextChangeData stores one or more text change data until notifying
+ // TSF of text change. If two or more text changes occur, this merges
+ // every text change data.
+ TextChangeData mPendingTextChangeData;
+
+ void NotifyTSFOfTextChange();
+ void NotifyTSFOfSelectionChange();
+ bool NotifyTSFOfLayoutChange();
+ void NotifyTSFOfLayoutChangeAgain();
+
+ HRESULT HandleRequestAttrs(DWORD aFlags, ULONG aFilterCount,
+ const TS_ATTRID* aFilterAttrs);
+ void SetInputScope(const nsString& aHTMLInputType,
+ const nsString& aHTMLInputInputmode,
+ bool aInPrivateBrowsing);
+
+ // Creates native caret over our caret. This method only works on desktop
+ // application. Otherwise, this does nothing.
+ void CreateNativeCaret();
+ // Destroys native caret if there is.
+ void MaybeDestroyNativeCaret();
+
+ /**
+ * MaybeHackNoErrorLayoutBugs() is a helper method of GetTextExt(). In
+ * strictly speaking, TSF is aware of asynchronous layout computation like us.
+ * However, Windows 10 version 1803 and older (including Windows 8.1 and
+ * older) Windows has a bug which is that the caller of GetTextExt() of TSF
+ * does not return TS_E_NOLAYOUT to TIP as is. Additionally, even after
+ * fixing this bug, some TIPs are not work well when we return TS_E_NOLAYOUT.
+ * For avoiding this issue, this method checks current Windows version and
+ * active TIP, and if in case we cannot return TS_E_NOLAYOUT, this modifies
+ * aACPStart and aACPEnd to making sure that they are in range of unmodified
+ * characters.
+ *
+ * @param aACPStart Initial value should be acpStart of GetTextExt().
+ * If this method returns true, this may be modified
+ * to be in range of unmodified characters.
+ * @param aACPEnd Initial value should be acpEnd of GetTextExt().
+ * If this method returns true, this may be modified
+ * to be in range of unmodified characters.
+ * And also this may become same as aACPStart.
+ * @return true if the caller shouldn't return TS_E_NOLAYOUT.
+ * In this case, this method modifies aACPStart and/or
+ * aASCPEnd to compute rectangle of unmodified characters.
+ * false if the caller can return TS_E_NOLAYOUT or
+ * we cannot have proper unmodified characters.
+ */
+ bool MaybeHackNoErrorLayoutBugs(LONG& aACPStart, LONG& aACPEnd);
+
+ // Holds the pointer to our current win32 widget
+ RefPtr<nsWindowBase> mWidget;
+ // mDispatcher is a helper class to dispatch composition events.
+ RefPtr<TextEventDispatcher> mDispatcher;
+ // Document manager for the currently focused editor
+ RefPtr<ITfDocumentMgr> mDocumentMgr;
+ // Edit cookie associated with the current editing context
+ DWORD mEditCookie;
+ // Editing context at the bottom of mDocumentMgr's context stack
+ RefPtr<ITfContext> mContext;
+ // Currently installed notification sink
+ RefPtr<ITextStoreACPSink> mSink;
+ // TS_AS_* mask of what events to notify
+ DWORD mSinkMask;
+ // 0 if not locked, otherwise TS_LF_* indicating the current lock
+ DWORD mLock;
+ // 0 if no lock is queued, otherwise TS_LF_* indicating the queue lock
+ DWORD mLockQueued;
+
+ uint32_t mHandlingKeyMessage;
+ void OnStartToHandleKeyMessage() {
+ // If we're starting to handle another key message during handling a
+ // key message, let's assume that the handling key message is handled by
+ // TIP and it sends another key message for hacking something.
+ // Let's try to dispatch a keyboard event now.
+ // FYI: All callers of this method grab this instance with local variable.
+ // So, even after calling MaybeDispatchKeyboardEventAsProcessedByIME(),
+ // we're safe to access any members.
+ if (!mDestroyed && sHandlingKeyMsg && !sIsKeyboardEventDispatched) {
+ MaybeDispatchKeyboardEventAsProcessedByIME();
+ }
+ ++mHandlingKeyMessage;
+ }
+ void OnEndHandlingKeyMessage(bool aIsProcessedByTSF) {
+ // If sHandlingKeyMsg has been handled by TSF or TIP and we're still
+ // alive, but we haven't dispatch keyboard event for it, let's fire it now.
+ // FYI: All callers of this method grab this instance with local variable.
+ // So, even after calling MaybeDispatchKeyboardEventAsProcessedByIME(),
+ // we're safe to access any members.
+ if (!mDestroyed && sHandlingKeyMsg && aIsProcessedByTSF &&
+ !sIsKeyboardEventDispatched) {
+ MaybeDispatchKeyboardEventAsProcessedByIME();
+ }
+ MOZ_ASSERT(mHandlingKeyMessage);
+ if (--mHandlingKeyMessage) {
+ return;
+ }
+ // If TSFTextStore instance is destroyed during handling key message(s),
+ // release all TSF objects when all nested key messages have been handled.
+ if (mDestroyed) {
+ ReleaseTSFObjects();
+ }
+ }
+
+ /**
+ * MaybeDispatchKeyboardEventAsProcessedByIME() tries to dispatch eKeyDown
+ * event or eKeyUp event for sHandlingKeyMsg and marking the dispatching
+ * event as "processed by IME". Note that if the document is locked, this
+ * just adds a pending action into the queue and sets
+ * sIsKeyboardEventDispatched to true.
+ */
+ void MaybeDispatchKeyboardEventAsProcessedByIME();
+
+ /**
+ * DispatchKeyboardEventAsProcessedByIME() dispatches an eKeyDown or
+ * eKeyUp event with NativeKey class and aMsg.
+ */
+ void DispatchKeyboardEventAsProcessedByIME(const MSG& aMsg);
+
+ // Composition class stores a copy of the active composition string. Only
+ // the data is updated during an InsertTextAtSelection call if we have a
+ // composition. The data acts as a buffer until OnUpdateComposition is
+ // called and the data is flushed to editor through eCompositionChange.
+ // This allows all changes to be updated in batches to avoid inconsistencies
+ // and artifacts.
+ class Composition final : public OffsetAndData<LONG> {
+ public:
+ explicit Composition(ITfCompositionView* aCompositionView,
+ LONG aCompositionStartOffset,
+ const nsAString& aCompositionString)
+ : OffsetAndData<LONG>(aCompositionStartOffset, aCompositionString),
+ mView(aCompositionView) {}
+
+ ITfCompositionView* GetView() const { return mView; }
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const Composition& aComposition) {
+ aStream << "{ mView=0x" << aComposition.mView.get()
+ << ", OffsetAndData<LONG>="
+ << static_cast<const OffsetAndData<LONG>&>(aComposition) << " }";
+ return aStream;
+ }
+
+ private:
+ RefPtr<ITfCompositionView> const mView;
+ };
+ // While the document is locked, we cannot dispatch any events which cause
+ // DOM events since the DOM events' handlers may modify the locked document.
+ // However, even while the document is locked, TSF may queries us.
+ // For that, TSFTextStore modifies mComposition even while the document is
+ // locked. With mComposition, query methods can returns the text content
+ // information.
+ Maybe<Composition> mComposition;
+
+ /**
+ * IsHandlingCompositionInParent() returns true if eCompositionStart is
+ * dispatched, but eCompositionCommit(AsIs) is not dispatched. This means
+ * that if composition is handled in a content process, this status indicates
+ * whether ContentCacheInParent has composition or not. On the other hand,
+ * if it's handled in the chrome process, this is exactly same as
+ * IsHandlingCompositionInContent().
+ */
+ bool IsHandlingCompositionInParent() const {
+ return mDispatcher && mDispatcher->IsComposing();
+ }
+
+ /**
+ * IsHandlingCompositionInContent() returns true if there is a composition in
+ * the focused editor which may be in a content process.
+ */
+ bool IsHandlingCompositionInContent() const {
+ return mDispatcher && mDispatcher->IsHandlingComposition();
+ }
+
+ class Selection {
+ public:
+ const TS_SELECTION_ACP& ACPRef() const { return mACP; }
+
+ explicit Selection(const TS_SELECTION_ACP& aSelection) {
+ SetSelection(aSelection);
+ }
+
+ explicit Selection(uint32_t aOffsetToCollapse) {
+ Collapse(aOffsetToCollapse);
+ }
+
+ explicit Selection(uint32_t aStart, uint32_t aLength, bool aReversed,
+ const WritingMode& aWritingMode) {
+ SetSelection(aStart, aLength, aReversed, aWritingMode);
+ }
+
+ void SetSelection(const TS_SELECTION_ACP& aSelection) {
+ mACP = aSelection;
+ // Selection end must be active in our editor.
+ if (mACP.style.ase != TS_AE_START) {
+ mACP.style.ase = TS_AE_END;
+ }
+ // We're not support interim char selection for now.
+ // XXX Probably, this is necessary for supporting South Asian languages.
+ mACP.style.fInterimChar = FALSE;
+ }
+
+ bool SetSelection(uint32_t aStart, uint32_t aLength, bool aReversed,
+ const WritingMode& aWritingMode) {
+ bool changed = mACP.acpStart != static_cast<LONG>(aStart) ||
+ mACP.acpEnd != static_cast<LONG>(aStart + aLength);
+ mACP.acpStart = static_cast<LONG>(aStart);
+ mACP.acpEnd = static_cast<LONG>(aStart + aLength);
+ mACP.style.ase = aReversed ? TS_AE_START : TS_AE_END;
+ mACP.style.fInterimChar = FALSE;
+ mWritingMode = aWritingMode;
+
+ return changed;
+ }
+
+ bool Collapsed() const { return mACP.acpStart == mACP.acpEnd; }
+
+ void Collapse(uint32_t aOffset) {
+ // XXX This does not update the selection's mWritingMode.
+ // If it is ever used to "collapse" to an entirely new location,
+ // we may need to fix that.
+ mACP.acpStart = mACP.acpEnd = static_cast<LONG>(aOffset);
+ mACP.style.ase = TS_AE_END;
+ mACP.style.fInterimChar = FALSE;
+ }
+
+ LONG MinOffset() const {
+ LONG min = std::min(mACP.acpStart, mACP.acpEnd);
+ MOZ_ASSERT(min >= 0);
+ return min;
+ }
+
+ LONG MaxOffset() const {
+ LONG max = std::max(mACP.acpStart, mACP.acpEnd);
+ MOZ_ASSERT(max >= 0);
+ return max;
+ }
+
+ LONG StartOffset() const {
+ MOZ_ASSERT(mACP.acpStart >= 0);
+ return mACP.acpStart;
+ }
+
+ LONG EndOffset() const {
+ MOZ_ASSERT(mACP.acpEnd >= 0);
+ return mACP.acpEnd;
+ }
+
+ LONG Length() const {
+ MOZ_ASSERT(mACP.acpEnd >= mACP.acpStart);
+ return std::abs(mACP.acpEnd - mACP.acpStart);
+ }
+
+ bool IsReversed() const { return mACP.style.ase == TS_AE_START; }
+
+ TsActiveSelEnd ActiveSelEnd() const { return mACP.style.ase; }
+
+ bool IsInterimChar() const { return mACP.style.fInterimChar != FALSE; }
+
+ WritingMode GetWritingMode() const { return mWritingMode; }
+
+ bool EqualsExceptDirection(const TS_SELECTION_ACP& aACP) const {
+ if (mACP.style.ase == aACP.style.ase) {
+ return mACP.acpStart == aACP.acpStart && mACP.acpEnd == aACP.acpEnd;
+ }
+ return mACP.acpStart == aACP.acpEnd && mACP.acpEnd == aACP.acpStart;
+ }
+
+ bool EqualsExceptDirection(
+ const SelectionChangeDataBase& aChangedSelection) const {
+ MOZ_ASSERT(aChangedSelection.IsValid());
+ return aChangedSelection.Length() == static_cast<uint32_t>(Length()) &&
+ aChangedSelection.mOffset == static_cast<uint32_t>(StartOffset());
+ }
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const Selection& aSelection) {
+ aStream << "{ mACP=" << ToString(aSelection.mACP).c_str()
+ << ", mWritingMode=" << ToString(aSelection.mWritingMode).c_str()
+ << ", Collapsed()="
+ << (aSelection.Collapsed() ? "true" : "false")
+ << ", Length=" << aSelection.Length() << " }";
+ return aStream;
+ }
+
+ private:
+ TS_SELECTION_ACP mACP;
+ WritingMode mWritingMode;
+ };
+ // Don't access mSelection directly. Instead, Use SelectionForTSFRef().
+ // This is modified immediately when TSF requests to set selection and not
+ // updated by selection change in content until mContentForTSF is cleared.
+ Maybe<Selection> mSelectionForTSF;
+
+ /**
+ * Get the selection expected by TSF. If mSelectionForTSF is already valid,
+ * this just return the reference to it. Otherwise, this initializes it
+ * with eQuerySelectedText. Please check if the result is valid before
+ * actually using it.
+ * Note that this is also called by ContentForTSF().
+ */
+ Maybe<Selection>& SelectionForTSF();
+
+ struct PendingAction final {
+ enum class Type : uint8_t {
+ eCompositionStart,
+ eCompositionUpdate,
+ eCompositionEnd,
+ eSetSelection,
+ eKeyboardEvent,
+ };
+ Type mType;
+ // For eCompositionStart, eCompositionEnd and eSetSelection
+ LONG mSelectionStart;
+ // For eCompositionStart and eSetSelection
+ LONG mSelectionLength;
+ // For eCompositionStart, eCompositionUpdate and eCompositionEnd
+ nsString mData;
+ // For eCompositionUpdate
+ RefPtr<TextRangeArray> mRanges;
+ // For eKeyboardEvent
+ MSG mKeyMsg;
+ // For eSetSelection
+ bool mSelectionReversed;
+ // For eCompositionUpdate
+ bool mIncomplete;
+ // For eCompositionStart
+ bool mAdjustSelection;
+ };
+ // Items of mPendingActions are appended when TSF tells us to need to dispatch
+ // DOM composition events. However, we cannot dispatch while the document is
+ // locked because it can cause modifying the locked document. So, the pending
+ // actions should be performed when document lock is unlocked.
+ nsTArray<PendingAction> mPendingActions;
+
+ PendingAction* LastOrNewPendingCompositionUpdate() {
+ if (!mPendingActions.IsEmpty()) {
+ PendingAction& lastAction = mPendingActions.LastElement();
+ if (lastAction.mType == PendingAction::Type::eCompositionUpdate) {
+ return &lastAction;
+ }
+ }
+ PendingAction* newAction = mPendingActions.AppendElement();
+ newAction->mType = PendingAction::Type::eCompositionUpdate;
+ newAction->mRanges = new TextRangeArray();
+ newAction->mIncomplete = true;
+ return newAction;
+ }
+
+ /**
+ * IsLastPendingActionCompositionEndAt() checks whether the previous pending
+ * action is committing composition whose range starts from aStart and its
+ * length is aLength. In other words, this checks whether new composition
+ * which will replace same range as previous pending commit can be merged
+ * with the previous composition.
+ *
+ * @param aStart The inserted offset you expected.
+ * @param aLength The inserted text length you expected.
+ * @return true if the last pending action is
+ * eCompositionEnd and it inserted the text
+ * between aStart and aStart + aLength.
+ */
+ bool IsLastPendingActionCompositionEndAt(LONG aStart, LONG aLength) const {
+ if (mPendingActions.IsEmpty()) {
+ return false;
+ }
+ const PendingAction& pendingLastAction = mPendingActions.LastElement();
+ return pendingLastAction.mType == PendingAction::Type::eCompositionEnd &&
+ pendingLastAction.mSelectionStart == aStart &&
+ pendingLastAction.mData.Length() == static_cast<ULONG>(aLength);
+ }
+
+ bool IsPendingCompositionUpdateIncomplete() const {
+ if (mPendingActions.IsEmpty()) {
+ return false;
+ }
+ const PendingAction& lastAction = mPendingActions.LastElement();
+ return lastAction.mType == PendingAction::Type::eCompositionUpdate &&
+ lastAction.mIncomplete;
+ }
+
+ void CompleteLastActionIfStillIncomplete() {
+ if (!IsPendingCompositionUpdateIncomplete()) {
+ return;
+ }
+ RecordCompositionUpdateAction();
+ }
+
+ void RemoveLastCompositionUpdateActions() {
+ while (!mPendingActions.IsEmpty()) {
+ const PendingAction& lastAction = mPendingActions.LastElement();
+ if (lastAction.mType != PendingAction::Type::eCompositionUpdate) {
+ break;
+ }
+ mPendingActions.RemoveLastElement();
+ }
+ }
+
+ // When On*Composition() is called without document lock, we need to flush
+ // the recorded actions at quitting the method.
+ // AutoPendingActionAndContentFlusher class is usedful for it.
+ class MOZ_STACK_CLASS AutoPendingActionAndContentFlusher final {
+ public:
+ explicit AutoPendingActionAndContentFlusher(TSFTextStore* aTextStore)
+ : mTextStore(aTextStore) {
+ MOZ_ASSERT(!mTextStore->mIsRecordingActionsWithoutLock);
+ if (!mTextStore->IsReadWriteLocked()) {
+ mTextStore->mIsRecordingActionsWithoutLock = true;
+ }
+ }
+
+ ~AutoPendingActionAndContentFlusher() {
+ if (!mTextStore->mIsRecordingActionsWithoutLock) {
+ return;
+ }
+ mTextStore->FlushPendingActions();
+ mTextStore->mIsRecordingActionsWithoutLock = false;
+ }
+
+ private:
+ AutoPendingActionAndContentFlusher() {}
+
+ RefPtr<TSFTextStore> mTextStore;
+ };
+
+ class Content final {
+ public:
+ Content(TSFTextStore& aTSFTextStore, const nsAString& aText)
+ : mText(aText),
+ mLastComposition(aTSFTextStore.mComposition),
+ mComposition(aTSFTextStore.mComposition),
+ mSelection(aTSFTextStore.mSelectionForTSF) {}
+
+ void OnLayoutChanged() { mMinModifiedOffset.reset(); }
+
+ // OnCompositionEventsHandled() is called when all pending composition
+ // events are handled in the focused content which may be in a remote
+ // process.
+ void OnCompositionEventsHandled() { mLastComposition = mComposition; }
+
+ const nsDependentSubstring GetSelectedText() const;
+ const nsDependentSubstring GetSubstring(uint32_t aStart,
+ uint32_t aLength) const;
+ void ReplaceSelectedTextWith(const nsAString& aString);
+ void ReplaceTextWith(LONG aStart, LONG aLength, const nsAString& aString);
+
+ void StartComposition(ITfCompositionView* aCompositionView,
+ const PendingAction& aCompStart,
+ bool aPreserveSelection);
+ /**
+ * RestoreCommittedComposition() restores the committed string as
+ * composing string. If InsertTextAtSelection() or something is called
+ * before a call of OnStartComposition() or previous composition is
+ * committed and new composition is restarted to clean up the commited
+ * string, there is a pending compositionend. In this case, we need to
+ * cancel the pending compositionend and continue the composition.
+ *
+ * @param aCompositionView The composition view.
+ * @param aCanceledCompositionEnd The pending compositionend which is
+ * canceled for restarting the composition.
+ */
+ void RestoreCommittedComposition(
+ ITfCompositionView* aCompositionView,
+ const PendingAction& aCanceledCompositionEnd);
+ void EndComposition(const PendingAction& aCompEnd);
+
+ const nsString& TextRef() const { return mText; }
+ const Maybe<OffsetAndData<LONG>>& LastComposition() const {
+ return mLastComposition;
+ }
+ const Maybe<uint32_t>& MinModifiedOffset() const {
+ return mMinModifiedOffset;
+ }
+ const Maybe<StartAndEndOffsets<LONG>>& LatestCompositionRange() const {
+ return mLatestCompositionRange;
+ }
+
+ // Returns true if layout of the character at the aOffset has not been
+ // calculated.
+ bool IsLayoutChangedAt(uint32_t aOffset) const {
+ return IsLayoutChanged() && (mMinModifiedOffset.value() <= aOffset);
+ }
+ // Returns true if layout of the content has been changed, i.e., the new
+ // layout has not been calculated.
+ bool IsLayoutChanged() const { return mMinModifiedOffset.isSome(); }
+ bool HasOrHadComposition() const {
+ return mLatestCompositionRange.isSome();
+ }
+
+ Maybe<TSFTextStore::Composition>& Composition() { return mComposition; }
+ Maybe<TSFTextStore::Selection>& Selection() { return mSelection; }
+
+ friend std::ostream& operator<<(std::ostream& aStream,
+ const Content& aContent) {
+ aStream << "{ mText="
+ << PrintStringDetail(aContent.mText,
+ PrintStringDetail::kMaxLengthForEditor)
+ .get()
+ << ", mLastComposition=" << aContent.mLastComposition
+ << ", mLatestCompositionRange="
+ << aContent.mLatestCompositionRange
+ << ", mMinModifiedOffset=" << aContent.mMinModifiedOffset << " }";
+ return aStream;
+ }
+
+ private:
+ nsString mText;
+
+ // mLastComposition may store the composition string and its start offset
+ // when the document is locked. This is necessary to compute
+ // mMinTextModifiedOffset.
+ Maybe<OffsetAndData<LONG>> mLastComposition;
+
+ Maybe<TSFTextStore::Composition>& mComposition;
+ Maybe<TSFTextStore::Selection>& mSelection;
+
+ // The latest composition's start and end offset.
+ Maybe<StartAndEndOffsets<LONG>> mLatestCompositionRange;
+
+ // The minimum offset of modified part of the text.
+ Maybe<uint32_t> mMinModifiedOffset;
+ };
+ // mContentForTSF is cache of content. The information is expected by TSF
+ // and TIP. Therefore, this is useful for answering the query from TSF or
+ // TIP.
+ // This is initialized by ContentForTSF() automatically (therefore, don't
+ // access this member directly except at calling Clear(), IsInitialized(),
+ // IsLayoutChangeAfter() or IsLayoutChanged()).
+ // This is cleared when:
+ // - When there is no composition, the document is unlocked.
+ // - When there is a composition, all dispatched events are handled by
+ // the focused editor which may be in a remote process.
+ // So, if two compositions are created very quickly, this cache may not be
+ // cleared between eCompositionCommit(AsIs) and eCompositionStart.
+ Maybe<Content> mContentForTSF;
+
+ Maybe<Content>& ContentForTSF();
+
+ // CanAccessActualContentDirectly() returns true when TSF/TIP can access
+ // actual content directly. In other words, mContentForTSF and/or
+ // mSelectionForTSF doesn't cache content or they matches with actual
+ // contents due to no pending text/selection change notifications.
+ bool CanAccessActualContentDirectly() const;
+
+ // While mContentForTSF is valid, this returns the text stored by it.
+ // Otherwise, return the current text content retrieved by eQueryTextContent.
+ bool GetCurrentText(nsAString& aTextContent);
+
+ class MouseTracker final {
+ public:
+ static const DWORD kInvalidCookie = static_cast<DWORD>(-1);
+
+ MouseTracker();
+
+ HRESULT Init(TSFTextStore* aTextStore);
+ HRESULT AdviseSink(TSFTextStore* aTextStore, ITfRangeACP* aTextRange,
+ ITfMouseSink* aMouseSink);
+ void UnadviseSink();
+
+ bool IsUsing() const { return mSink != nullptr; }
+ DWORD Cookie() const { return mCookie; }
+ bool OnMouseButtonEvent(ULONG aEdge, ULONG aQuadrant, DWORD aButtonStatus);
+ const Maybe<StartAndEndOffsets<LONG>> Range() const { return mRange; }
+
+ private:
+ RefPtr<ITfMouseSink> mSink;
+ Maybe<StartAndEndOffsets<LONG>> mRange;
+ DWORD mCookie;
+ };
+ // mMouseTrackers is an array to store each information of installed
+ // ITfMouseSink instance.
+ nsTArray<MouseTracker> mMouseTrackers;
+
+ // The input scopes for this context, defaults to IS_DEFAULT.
+ nsTArray<InputScope> mInputScopes;
+
+ // Support retrieving attributes.
+ // TODO: We should support RightToLeft, perhaps.
+ enum {
+ // Used for result of GetRequestedAttrIndex()
+ eNotSupported = -1,
+
+ // Supported attributes
+ eInputScope = 0,
+ eTextVerticalWriting,
+ eTextOrientation,
+
+ // Count of the supported attributes
+ NUM_OF_SUPPORTED_ATTRS
+ };
+ bool mRequestedAttrs[NUM_OF_SUPPORTED_ATTRS];
+
+ int32_t GetRequestedAttrIndex(const TS_ATTRID& aAttrID);
+ TS_ATTRID GetAttrID(int32_t aIndex);
+
+ bool mRequestedAttrValues;
+
+ // If edit actions are being recorded without document lock, this is true.
+ // Otherwise, false.
+ bool mIsRecordingActionsWithoutLock;
+ // If GetTextExt() or GetACPFromPoint() is called and the layout hasn't been
+ // calculated yet, these methods return TS_E_NOLAYOUT. At that time,
+ // mHasReturnedNoLayoutError is set to true.
+ bool mHasReturnedNoLayoutError;
+ // Before calling ITextStoreACPSink::OnLayoutChange() and
+ // ITfContextOwnerServices::OnLayoutChange(), mWaitingQueryLayout is set to
+ // true. This is set to false when GetTextExt() or GetACPFromPoint() is
+ // called.
+ bool mWaitingQueryLayout;
+ // During the documet is locked, we shouldn't destroy the instance.
+ // If this is true, the instance will be destroyed after unlocked.
+ bool mPendingDestroy;
+ // If this is false, MaybeFlushPendingNotifications() will clear the
+ // mContentForTSF.
+ bool mDeferClearingContentForTSF;
+ // While the instance is dispatching events, the event may not be handled
+ // synchronously in e10s mode. So, in such case, in strictly speaking,
+ // we shouldn't query layout information. However, TS_E_NOLAYOUT bugs of
+ // ITextStoreAPC::GetTextExt() blocks us to behave ideally.
+ // For preventing it to be called, we should put off notifying TSF of
+ // anything until layout information becomes available.
+ bool mDeferNotifyingTSF;
+ // While the document is locked, committing composition always fails since
+ // TSF needs another document lock for modifying the composition, selection
+ // and etc. So, committing composition should be performed after the
+ // document is unlocked.
+ bool mDeferCommittingComposition;
+ bool mDeferCancellingComposition;
+ // Immediately after a call of Destroy(), mDestroyed becomes true. If this
+ // is true, the instance shouldn't grant any requests from the TIP anymore.
+ bool mDestroyed;
+ // While the instance is being destroyed, this is set to true for avoiding
+ // recursive Destroy() calls.
+ bool mBeingDestroyed;
+
+ // TSF thread manager object for the current application
+ static StaticRefPtr<ITfThreadMgr> sThreadMgr;
+ static already_AddRefed<ITfThreadMgr> GetThreadMgr();
+ // sMessagePump is QI'ed from sThreadMgr
+ static StaticRefPtr<ITfMessagePump> sMessagePump;
+
+ public:
+ // Expose GetMessagePump() for WinUtils.
+ static already_AddRefed<ITfMessagePump> GetMessagePump();
+
+ private:
+ // sKeystrokeMgr is QI'ed from sThreadMgr
+ static StaticRefPtr<ITfKeystrokeMgr> sKeystrokeMgr;
+ // TSF display attribute manager
+ static StaticRefPtr<ITfDisplayAttributeMgr> sDisplayAttrMgr;
+ static already_AddRefed<ITfDisplayAttributeMgr> GetDisplayAttributeMgr();
+ // TSF category manager
+ static StaticRefPtr<ITfCategoryMgr> sCategoryMgr;
+ static already_AddRefed<ITfCategoryMgr> GetCategoryMgr();
+ // Compartment for (Get|Set)IMEOpenState()
+ static StaticRefPtr<ITfCompartment> sCompartmentForOpenClose;
+ static already_AddRefed<ITfCompartment> GetCompartmentForOpenClose();
+
+ // Current text store which is managing a keyboard enabled editor (i.e.,
+ // editable editor). Currently only ONE TSFTextStore instance is ever used,
+ // although Create is called when an editor is focused and Destroy called
+ // when the focused editor is blurred.
+ static StaticRefPtr<TSFTextStore> sEnabledTextStore;
+
+ // For IME (keyboard) disabled state:
+ static StaticRefPtr<ITfDocumentMgr> sDisabledDocumentMgr;
+ static StaticRefPtr<ITfContext> sDisabledContext;
+
+ static StaticRefPtr<ITfInputProcessorProfiles> sInputProcessorProfiles;
+ static already_AddRefed<ITfInputProcessorProfiles>
+ GetInputProcessorProfiles();
+
+ // Handling key message.
+ static const MSG* sHandlingKeyMsg;
+
+ // TSF client ID for the current application
+ static DWORD sClientId;
+
+ // true if an eKeyDown or eKeyUp event for sHandlingKeyMsg has already
+ // been dispatched.
+ static bool sIsKeyboardEventDispatched;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // #ifndef TSFTextStore_h_