/* -*- 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 "nsWindow.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/TextEvents.h" #include "mozilla/TextRange.h" #include "mozilla/WindowsVersion.h" #include "mozilla/widget/IMEData.h" #include #include // 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 #endif #ifdef TEXTATTRS_INIT_GUID # include #endif #include // TSF InputScope, for earlier SDK 8 #define IS_SEARCH static_cast(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(nsWindow* 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 textStore(sEnabledTextStore); textStore->CommitCompositionInternal(aDiscard); } static void SetInputContext(nsWindow* aWidget, const InputContext& aContext, const InputContextAction& aAction); static nsresult OnFocusChange(bool aGotFocus, nsWindow* 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 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 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 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 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 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(&sThreadMgr); case NS_NATIVE_TSF_CATEGORY_MGR: return static_cast(&sCategoryMgr); case NS_NATIVE_TSF_DISPLAY_ATTR_MGR: return static_cast(&sDisplayAttrMgr); default: return nullptr; } } static void* GetThreadManager() { return static_cast(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(nsWindow* aWidget) { return (IsComposing() && sEnabledTextStore->mWidget == aWidget); } static nsWindow* 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 is ATOK. */ static bool IsATOKActive(); /** * 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(nsWindow* aFocusedWidget, const InputContext& aContext); static void EnsureToDestroyAndReleaseEnabledTextStoreIf( RefPtr& aTextStore); static void MarkContextAsKeyboardDisabled(ITfContext* aContext); static void MarkContextAsEmpty(ITfContext* aContext); bool Init(nsWindow* 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. Maybe 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 mWidget; // mDispatcher is a helper class to dispatch composition events. RefPtr mDispatcher; // Document manager for the currently focused editor RefPtr mDocumentMgr; // Edit cookie associated with the current editing context DWORD mEditCookie; // Editing context at the bottom of mDocumentMgr's context stack RefPtr mContext; // Currently installed notification sink RefPtr 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 { public: explicit Composition(ITfCompositionView* aCompositionView, LONG aCompositionStartOffset, const nsAString& aCompositionString) : OffsetAndData(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=" << static_cast&>(aComposition) << " }"; return aStream; } private: RefPtr 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 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: static TS_SELECTION_ACP EmptyACP() { return TS_SELECTION_ACP{ .acpStart = 0, .acpEnd = 0, .style = {.ase = TS_AE_NONE, .fInterimChar = FALSE}}; } bool HasRange() const { return mACP.isSome(); } const TS_SELECTION_ACP& ACPRef() const { return mACP.ref(); } explicit Selection(const TS_SELECTION_ACP& aSelection) { SetSelection(aSelection); } explicit Selection(uint32_t aOffsetToCollapse) { Collapse(aOffsetToCollapse); } explicit Selection(const SelectionChangeDataBase& aSelectionChangeData) { SetSelection(aSelectionChangeData); } explicit Selection(const WidgetQueryContentEvent& aQuerySelectionEvent) { SetSelection(aQuerySelectionEvent); } 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 = Some(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(const SelectionChangeDataBase& aSelectionChangeData) { MOZ_ASSERT(aSelectionChangeData.IsInitialized()); if (!aSelectionChangeData.HasRange()) { if (mACP.isNothing()) { return false; } mACP.reset(); // Let's keep the WritingMode because users don't want to change the UI // of TIP temporarily since no selection case is created only by web // apps, but they or TIP would restore selection at last point later. return true; } return SetSelection(aSelectionChangeData.mOffset, aSelectionChangeData.Length(), aSelectionChangeData.mReversed, aSelectionChangeData.GetWritingMode()); } bool SetSelection(const WidgetQueryContentEvent& aQuerySelectionEvent) { MOZ_ASSERT(aQuerySelectionEvent.mMessage == eQuerySelectedText); MOZ_ASSERT(aQuerySelectionEvent.Succeeded()); if (aQuerySelectionEvent.DidNotFindSelection()) { if (mACP.isNothing()) { return false; } mACP.reset(); // Let's keep the WritingMode because users don't want to change the UI // of TIP temporarily since no selection case is created only by web // apps, but they or TIP would restore selection at last point later. return true; } return SetSelection(aQuerySelectionEvent.mReply->StartOffset(), aQuerySelectionEvent.mReply->DataLength(), aQuerySelectionEvent.mReply->mReversed, aQuerySelectionEvent.mReply->WritingModeRef()); } bool SetSelection(uint32_t aStart, uint32_t aLength, bool aReversed, const WritingMode& aWritingMode) { const bool changed = mACP.isNothing() || mACP->acpStart != static_cast(aStart) || mACP->acpEnd != static_cast(aStart + aLength); mACP = Some( TS_SELECTION_ACP{.acpStart = static_cast(aStart), .acpEnd = static_cast(aStart + aLength), .style = {.ase = aReversed ? TS_AE_START : TS_AE_END, .fInterimChar = FALSE}}); mWritingMode = aWritingMode; return changed; } bool Collapsed() const { return mACP.isNothing() || 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 = Some( TS_SELECTION_ACP{.acpStart = static_cast(aOffset), .acpEnd = static_cast(aOffset), .style = {.ase = TS_AE_END, .fInterimChar = FALSE}}); } LONG MinOffset() const { MOZ_ASSERT(mACP.isSome()); LONG min = std::min(mACP->acpStart, mACP->acpEnd); MOZ_ASSERT(min >= 0); return min; } LONG MaxOffset() const { MOZ_ASSERT(mACP.isSome()); LONG max = std::max(mACP->acpStart, mACP->acpEnd); MOZ_ASSERT(max >= 0); return max; } LONG StartOffset() const { MOZ_ASSERT(mACP.isSome()); MOZ_ASSERT(mACP->acpStart >= 0); return mACP->acpStart; } LONG EndOffset() const { MOZ_ASSERT(mACP.isSome()); MOZ_ASSERT(mACP->acpEnd >= 0); return mACP->acpEnd; } LONG Length() const { MOZ_ASSERT(mACP->acpEnd >= mACP->acpStart); return mACP.isSome() ? std::abs(mACP->acpEnd - mACP->acpStart) : 0; } bool IsReversed() const { return mACP.isSome() && mACP->style.ase == TS_AE_START; } TsActiveSelEnd ActiveSelEnd() const { return mACP.isSome() ? mACP->style.ase : TS_AE_NONE; } bool IsInterimChar() const { return mACP.isSome() && mACP->style.fInterimChar != FALSE; } const WritingMode& WritingModeRef() const { return mWritingMode; } bool EqualsExceptDirection(const TS_SELECTION_ACP& aACP) const { if (mACP.isNothing()) { return false; } 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.IsInitialized()); if (mACP.isNothing()) { return aChangedSelection.HasRange(); } return aChangedSelection.Length() == static_cast(Length()) && aChangedSelection.mOffset == static_cast(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: Maybe mACP; // If Nothing, there is no selection 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 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& 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 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 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(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 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>& LastComposition() const { return mLastComposition; } const Maybe& MinModifiedOffset() const { return mMinModifiedOffset; } const Maybe>& 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& Composition() { return mComposition; } Maybe& 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> mLastComposition; Maybe& mComposition; Maybe& mSelection; // The latest composition's start and end offset. Maybe> mLatestCompositionRange; // The minimum offset of modified part of the text. Maybe 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 mContentForTSF; Maybe& 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(-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> Range() const { return mRange; } private: RefPtr mSink; Maybe> mRange; DWORD mCookie; }; // mMouseTrackers is an array to store each information of installed // ITfMouseSink instance. nsTArray mMouseTrackers; // The input scopes for this context, defaults to IS_DEFAULT. nsTArray 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 sThreadMgr; static already_AddRefed GetThreadMgr(); // sMessagePump is QI'ed from sThreadMgr static StaticRefPtr sMessagePump; public: // Expose GetMessagePump() for WinUtils. static already_AddRefed GetMessagePump(); private: // sKeystrokeMgr is QI'ed from sThreadMgr static StaticRefPtr sKeystrokeMgr; // TSF display attribute manager static StaticRefPtr sDisplayAttrMgr; static already_AddRefed GetDisplayAttributeMgr(); // TSF category manager static StaticRefPtr sCategoryMgr; static already_AddRefed GetCategoryMgr(); // Compartment for (Get|Set)IMEOpenState() static StaticRefPtr sCompartmentForOpenClose; static already_AddRefed 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 sEnabledTextStore; // For IME (keyboard) disabled state: static StaticRefPtr sDisabledDocumentMgr; static StaticRefPtr sDisabledContext; static StaticRefPtr sInputProcessorProfiles; static already_AddRefed 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_