/* -*- 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 NSTEXTSTORE_H_ #define NSTEXTSTORE_H_ #include "nsAutoPtr.h" #include "nsString.h" #include "nsCOMPtr.h" #include "nsITimer.h" #include "nsIWidget.h" #include "nsWindowBase.h" #include "mozilla/Attributes.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 #include struct ITfThreadMgr; struct ITfDocumentMgr; struct ITfDisplayAttributeMgr; struct ITfCategoryMgr; class nsWindow; class nsTextEvent; #ifdef MOZ_METRO class MetroWidget; #endif // It doesn't work well when we notify TSF of text change // during a mutation observer call because things get broken. // So we post a message and notify TSF when we get it later. #define WM_USER_TSF_TEXTCHANGE (WM_USER + 0x100) /* * Text Services Framework text store */ class nsTextStore MOZ_FINAL : public ITextStoreACP, public ITfContextOwnerCompositionSink { public: /*IUnknown*/ STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP QueryInterface(REFIID, void**); STDMETHODIMP_(ULONG) Release(void); 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*); protected: typedef mozilla::widget::IMEState IMEState; typedef mozilla::widget::InputContext InputContext; public: static void Initialize(void); static void Terminate(void); static void SetIMEOpenState(bool); static bool GetIMEOpenState(void); static void CommitComposition(bool aDiscard) { NS_ENSURE_TRUE_VOID(sTsfTextStore); sTsfTextStore->CommitCompositionInternal(aDiscard); } static void SetInputContext(const InputContext& aContext) { NS_ENSURE_TRUE_VOID(sTsfTextStore); sTsfTextStore->SetInputScope(aContext.mHTMLInputType); sTsfTextStore->SetInputContextInternal(aContext.mIMEState.mEnabled); } static nsresult OnFocusChange(bool aGotFocus, nsWindowBase* aFocusedWidget, IMEState::Enabled aIMEEnabled); static nsresult OnTextChange(uint32_t aStart, uint32_t aOldEnd, uint32_t aNewEnd) { NS_ENSURE_TRUE(sTsfTextStore, NS_ERROR_NOT_AVAILABLE); return sTsfTextStore->OnTextChangeInternal(aStart, aOldEnd, aNewEnd); } static void OnTextChangeMsg(void) { NS_ENSURE_TRUE_VOID(sTsfTextStore); // Notify TSF text change // (see comments on WM_USER_TSF_TEXTCHANGE in nsTextStore.h) sTsfTextStore->OnTextChangeMsgInternal(); } static nsresult OnSelectionChange(void) { NS_ENSURE_TRUE(sTsfTextStore, NS_ERROR_NOT_AVAILABLE); return sTsfTextStore->OnSelectionChangeInternal(); } static nsIMEUpdatePreference GetIMEUpdatePreference(); static bool CanOptimizeKeyAndIMEMessages() { // TODO: We need to implement this for ATOK. return true; } // Returns the address of the pointer so that the TSF automatic test can // replace the system object with a custom implementation for testing. static void* GetNativeData(uint32_t aDataType) { switch (aDataType) { case NS_NATIVE_TSF_THREAD_MGR: Initialize(); // Apply any previous changes return static_cast(&sTsfThreadMgr); 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* GetTextStore() { return static_cast(sTsfTextStore); } static bool ThinksHavingFocus() { return (sTsfTextStore && sTsfTextStore->mContext); } static bool IsInTSFMode() { return sTsfThreadMgr != nullptr; } static bool IsComposing() { return (sTsfTextStore && sTsfTextStore->mComposition.IsComposing()); } static bool IsComposingOn(nsWindowBase* aWidget) { return (IsComposing() && sTsfTextStore->mWidget == aWidget); } #ifdef DEBUG // Returns true when keyboard layout has IME (TIP). static bool CurrentKeyboardLayoutHasIME(); #endif // #ifdef DEBUG protected: nsTextStore(); ~nsTextStore(); bool Create(nsWindowBase* aWidget, IMEState::Enabled aIMEEnabled); bool Destroy(void); 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); } bool GetScreenExtInternal(RECT &aScreenExt); // If aDispatchTextEvent is true, this method will dispatch text event if // this is called during IME composing. aDispatchTextEvent should be true // only when this is called from SetSelection. Because otherwise, the text // event should not be sent from here. HRESULT SetSelectionInternal(const TS_SELECTION_ACP*, bool aDispatchTextEvent = false); bool InsertTextAtSelectionInternal(const nsAString &aInsertStr, TS_TEXTCHANGE* aTextChange); HRESULT OnStartCompositionInternal(ITfCompositionView*, ITfRange*, bool); void CommitCompositionInternal(bool); void SetInputContextInternal(IMEState::Enabled aState); nsresult OnTextChangeInternal(uint32_t, uint32_t, uint32_t); void OnTextChangeMsgInternal(void); nsresult OnSelectionChangeInternal(void); HRESULT GetDisplayAttribute(ITfProperty* aProperty, ITfRange* aRange, TF_DISPLAYATTRIBUTE* aResult); HRESULT RestartCompositionIfNecessary(ITfRange* pRangeNew = nullptr); HRESULT RecordCompositionUpdateAction(); void FlushPendingActions(); nsresult OnLayoutChange(); HRESULT ProcessScopeRequest(DWORD dwFlags, ULONG cFilterAttrs, const TS_ATTRID *paFilterAttrs); void SetInputScope(const nsString& aHTMLInputType); // Holds the pointer to our current win32 or metro widget nsRefPtr mWidget; // Document manager for the currently focused editor nsRefPtr mDocumentMgr; // Edit cookie associated with the current editing context DWORD mEditCookie; // Editing context at the bottom of mDocumentMgr's context stack nsRefPtr mContext; // Currently installed notification sink nsRefPtr 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; // Cumulative text change offsets since the last notification TS_TEXTCHANGE mTextChange; class Composition MOZ_FINAL { public: // NULL if no composition is active, otherwise the current composition nsRefPtr mView; // Current copy of the active composition string. Only mString is // changed during a InsertTextAtSelection call if we have a composition. // mString acts as a buffer until OnUpdateComposition is called // and mString is flushed to editor through NS_TEXT_TEXT. This // way all changes are updated in batches to avoid // inconsistencies/artifacts. nsString mString; // The latest composition string which was dispatched by composition update // event. nsString mLastData; // The start of the current active composition, in ACP offsets LONG mStart; bool IsComposing() const { return (mView != nullptr); } LONG EndOffset() const { return mStart + static_cast(mString.Length()); } // Start() and End() updates the members for emulating the latest state. // Unless flush the pending actions, this data never matches the actual // content. void Start(ITfCompositionView* aCompositionView, LONG aCompositionStartOffset, const nsAString& aCompositionString); void End(); void StartLayoutChangeTimer(nsTextStore* aTextStore); void EnsureLayoutChangeTimerStopped(); private: // Timer for calling ITextStoreACPSink::OnLayoutChange(). This is only used // during composing. nsCOMPtr mLayoutChangeTimer; static void TimerCallback(nsITimer* aTimer, void *aClosure); static uint32_t GetLayoutChangeIntervalTime(); }; // 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, nsTextStore modifies mComposition even while the document is // locked. With mComposition, query methods can returns the text content // information. Composition mComposition; class Selection { public: Selection() : mDirty(true) {} bool IsDirty() const { return mDirty; }; void MarkDirty() { mDirty = true; } TS_SELECTION_ACP& ACP() { MOZ_ASSERT(!mDirty); return mACP; } void SetSelection(const TS_SELECTION_ACP& aSelection) { mDirty = false; 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; } void SetSelection(uint32_t aStart, uint32_t aLength, bool aReversed) { mDirty = false; mACP.acpStart = static_cast(aStart); mACP.acpEnd = static_cast(aStart + aLength); mACP.style.ase = aReversed ? TS_AE_START : TS_AE_END; mACP.style.fInterimChar = FALSE; } bool IsCollapsed() const { MOZ_ASSERT(!mDirty); return (mACP.acpStart == mACP.acpEnd); } void CollapseAt(uint32_t aOffset) { mDirty = false; mACP.acpStart = mACP.acpEnd = static_cast(aOffset); mACP.style.ase = TS_AE_END; mACP.style.fInterimChar = FALSE; } LONG MinOffset() const { MOZ_ASSERT(!mDirty); LONG min = std::min(mACP.acpStart, mACP.acpEnd); MOZ_ASSERT(min >= 0); return min; } LONG MaxOffset() const { MOZ_ASSERT(!mDirty); LONG max = std::max(mACP.acpStart, mACP.acpEnd); MOZ_ASSERT(max >= 0); return max; } LONG StartOffset() const { MOZ_ASSERT(!mDirty); MOZ_ASSERT(mACP.acpStart >= 0); return mACP.acpStart; } LONG EndOffset() const { MOZ_ASSERT(!mDirty); MOZ_ASSERT(mACP.acpEnd >= 0); return mACP.acpEnd; } LONG Length() const { MOZ_ASSERT(!mDirty); MOZ_ASSERT(mACP.acpEnd >= mACP.acpStart); return std::abs(mACP.acpEnd - mACP.acpStart); } bool IsReversed() const { MOZ_ASSERT(!mDirty); return (mACP.style.ase == TS_AE_START); } TsActiveSelEnd ActiveSelEnd() const { MOZ_ASSERT(!mDirty); return mACP.style.ase; } bool IsInterimChar() const { MOZ_ASSERT(!mDirty); return (mACP.style.fInterimChar != FALSE); } private: TS_SELECTION_ACP mACP; bool mDirty; }; // Don't access mSelection directly except at calling MarkDirty(). // Use CurrentSelection() instead. This is marked as dirty when the // selection or content is changed without document lock. Selection mSelection; // Get "current selection" while the document is locked. The selection is // NOT modified immediately during document lock. The pending changes will // be flushed at unlocking the document. The "current selection" is the // modified selection during document lock. This is also called // CurrentContent() too. Selection& CurrentSelection(); struct PendingAction MOZ_FINAL { enum ActionType MOZ_ENUM_TYPE(uint8_t) { COMPOSITION_START, COMPOSITION_UPDATE, COMPOSITION_END, SELECTION_SET }; ActionType mType; // For compositionstart and selectionset LONG mSelectionStart; LONG mSelectionLength; // For compositionupdate and compositionend nsString mData; // For compositionupdate nsTArray mRanges; // For selectionset bool mSelectionReversed; }; // 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* GetPendingCompositionUpdate() { if (!mPendingActions.IsEmpty()) { PendingAction& lastAction = mPendingActions.LastElement(); if (lastAction.mType == PendingAction::COMPOSITION_UPDATE) { return &lastAction; } } PendingAction* newAction = mPendingActions.AppendElement(); newAction->mType = PendingAction::COMPOSITION_UPDATE; // We think that 4 ranges (3 clauses and caret position) are enough for // most cases. newAction->mRanges.SetCapacity(4); return newAction; } // 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 NS_STACK_CLASS AutoPendingActionAndContentFlusher MOZ_FINAL { public: AutoPendingActionAndContentFlusher(nsTextStore* 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() {} nsRefPtr mTextStore; }; class Content MOZ_FINAL { public: Content(nsTextStore::Composition& aComposition, nsTextStore::Selection& aSelection) : mComposition(aComposition), mSelection(aSelection) { Clear(); } void Clear() { mText.Truncate(); mInitialized = false; } bool IsInitialized() const { return mInitialized; } void Init(const nsAString& aText) { mText = aText; mMinTextModifiedOffset = NOT_MODIFIED; mInitialized = true; mNotifyTSFOfLayoutChange = false; } 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); void EndComposition(const PendingAction& aCompEnd); const nsString& Text() const { MOZ_ASSERT(mInitialized); return mText; } // Returns true if layout of the character at the aOffset has not been // calculated. bool IsLayoutChangedAfter(uint32_t aOffset) const { return mInitialized && (mMinTextModifiedOffset < aOffset); } // Returns true if layout of the content has been changed, i.e., the new // layout has not been calculated. bool IsLayoutChanged() const { return mInitialized && (mMinTextModifiedOffset != NOT_MODIFIED); } void NeedsToNotifyTSFOfLayoutChange() { mNotifyTSFOfLayoutChange = true; } bool NeedToNotifyTSFOfLayoutChange() const { return mInitialized && mNotifyTSFOfLayoutChange; } nsTextStore::Composition& Composition() { return mComposition; } nsTextStore::Selection& Selection() { return mSelection; } private: nsString mText; nsTextStore::Composition& mComposition; nsTextStore::Selection& mSelection; // The minimum offset of modified part of the text. enum MOZ_ENUM_TYPE(uint32_t) { NOT_MODIFIED = UINT32_MAX }; uint32_t mMinTextModifiedOffset; bool mInitialized; bool mNotifyTSFOfLayoutChange; }; // mContent caches "current content" of the document ONLY while the document // is locked. I.e., the content is cleared at unlocking the document since // we need to reduce the memory usage. This is initialized by // CurrentContent() automatically, so, don't access this member directly // except at calling Clear(), IsInitialized(), IsLayoutChangedAfter() or // IsLayoutChanged(). Content mContent; Content& CurrentContent(); // The input scopes for this context, defaults to IS_DEFAULT. nsTArray mInputScopes; bool mInputScopeDetected; bool mInputScopeRequested; // If edit actions are being recorded without document lock, this is true. // Otherwise, false. bool mIsRecordingActionsWithoutLock; // During recording actions, we shouldn't call mSink->OnSelectionChange() // because it may cause TSF request new lock. This is a problem if the // selection change is caused by a call of On*Composition() without document // lock since RequestLock() tries to flush the pending actions again (which // are flushing). Therefore, OnSelectionChangeInternal() sets this true // during recoding actions and then, FlushPendingActions() will call // mSink->OnSelectionChange(). bool mNotifySelectionChange; // TSF thread manager object for the current application static ITfThreadMgr* sTsfThreadMgr; // TSF display attribute manager static ITfDisplayAttributeMgr* sDisplayAttrMgr; // TSF category manager static ITfCategoryMgr* sCategoryMgr; // TSF client ID for the current application static DWORD sTsfClientId; // Current text store. Currently only ONE nsTextStore instance is ever used, // although Create is called when an editor is focused and Destroy called // when the focused editor is blurred. static nsTextStore* sTsfTextStore; // Message the Tablet Input Panel uses to flush text during blurring. // See comments in Destroy static UINT sFlushTIPInputMessage; private: ULONG mRefCnt; }; #endif /*NSTEXTSTORE_H_*/