/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- * * The contents of this file are subject to the Netscape Public License * Version 1.0 (the "NPL"); you may not use this file except in * compliance with the NPL. You may obtain a copy of the NPL at * http://www.mozilla.org/NPL/ * * Software distributed under the NPL is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the NPL * for the specific language governing rights and limitations under the * NPL. * * The Initial Developer of this code under the NPL is Netscape * Communications Corporation. Portions created by Netscape are * Copyright (C) 1998 Netscape Communications Corporation. All Rights * Reserved. */ #include "nsCOMPtr.h" #include "nsITimer.h" #include "nsITimerCallback.h" #include "nsIComponentManager.h" #include "nsIFrameSelection.h" #include "nsIFrame.h" #include "nsIDOMNode.h" #include "nsIDOMRange.h" #include "nsIDOMSelection.h" #include "nsIDOMCharacterData.h" #include "nsIContent.h" #include "nsIPresShell.h" #include "nsIRenderingContext.h" #include "nsIDeviceContext.h" #include "nsIView.h" #include "nsIViewManager.h" #include "nsIPresContext.h" #include "nsILookAndFeel.h" #include "nsWidgetsCID.h" // for NS_LOOKANDFEEL_CID #include "nsCaret.h" static NS_DEFINE_IID(kISupportsIID, NS_ISUPPORTS_IID); static NS_DEFINE_IID(kICaretID, NS_ICARET_IID); static NS_DEFINE_IID(kLookAndFeelCID, NS_LOOKANDFEEL_CID); //----------------------------------------------------------------------------- nsCaret::nsCaret() : mPresShell(nsnull) , mBlinkTimer(nsnull) , mBlinkRate(500) , mCaretWidth(20) , mVisible(PR_FALSE) , mReadOnly(PR_TRUE) , mDrawn(PR_FALSE) , mRendContext(nsnull) , mLastCaretFrame(nsnull) , mLastContentOffset(0) { NS_INIT_REFCNT(); } //----------------------------------------------------------------------------- nsCaret::~nsCaret() { KillTimer(); } //----------------------------------------------------------------------------- NS_IMETHODIMP nsCaret::Init(nsIPresShell *inPresShell) { if (!inPresShell) return NS_ERROR_NULL_POINTER; mPresShell = inPresShell; // the presshell owns us, so no addref nsILookAndFeel* touchyFeely; if (NS_SUCCEEDED(nsComponentManager::CreateInstance(kLookAndFeelCID, nsnull, nsILookAndFeel::GetIID(), (void**)&touchyFeely))) { PRInt32 tempInt; if (NS_SUCCEEDED(touchyFeely->GetMetric(nsILookAndFeel::eMetric_CaretWidthTwips, tempInt))) mCaretWidth = (nscoord)tempInt; if (NS_SUCCEEDED(touchyFeely->GetMetric(nsILookAndFeel::eMetric_CaretBlinkTime, tempInt))) mBlinkRate = (PRUint32)tempInt; NS_RELEASE(touchyFeely); } // get the selection from the pres shell, and set ourselves up as a selection // listener nsCOMPtr domSelection; if (NS_SUCCEEDED(mPresShell->GetSelection(getter_AddRefs(domSelection)))) { domSelection->AddSelectionListener(this); } // set up the blink timer if (mVisible) { nsresult err = StartBlinking(); if (NS_FAILED(err)) return err; } return NS_OK; } //----------------------------------------------------------------------------- NS_IMPL_ADDREF(nsCaret); NS_IMPL_RELEASE(nsCaret); //----------------------------------------------------------------------------- NS_IMETHODIMP nsCaret::QueryInterface(const nsIID& aIID, void** aInstancePtrResult) { NS_PRECONDITION(nsnull != aInstancePtrResult, "null pointer"); if (nsnull == aInstancePtrResult) { return NS_ERROR_NULL_POINTER; } if (aIID.Equals(kISupportsIID)) { *aInstancePtrResult = (void*)(nsISupports*)(nsICaret*)this; // whoo boy NS_ADDREF_THIS(); return NS_OK; } if (aIID.Equals(nsICaret::GetIID())) { *aInstancePtrResult = (void*)(nsICaret*)this; NS_ADDREF_THIS(); return NS_OK; } if (aIID.Equals(nsIDOMSelectionListener::GetIID())) { *aInstancePtrResult = (void*)(nsIDOMSelectionListener*)this; NS_ADDREF_THIS(); return NS_OK; } return NS_NOINTERFACE; } //----------------------------------------------------------------------------- NS_IMETHODIMP nsCaret::SetCaretVisible(PRBool inMakeVisible) { mVisible = inMakeVisible; nsresult err = NS_OK; if (mVisible) err = StartBlinking(); else err = StopBlinking(); return err; } //----------------------------------------------------------------------------- NS_IMETHODIMP nsCaret::SetCaretReadOnly(PRBool inMakeReadonly) { mReadOnly = inMakeReadonly; return NS_OK; } //----------------------------------------------------------------------------- NS_IMETHODIMP nsCaret::Refresh(nsIView *aView, nsIRenderingContext& inRendContext, const nsRect& aDirtyRect) { // a refresh is different from a simple redraw, in that it does not affect // the timer, or toggle the drawn status. It's used to redraw the caret // when painting happens, beacuse this will have drawn over the caret. if (mVisible && mDrawn) { RefreshDrawCaret(aView, inRendContext, aDirtyRect); } return NS_OK; } //----------------------------------------------------------------------------- NS_IMETHODIMP nsCaret::GetWindowRelativeCoordinates(nsPoint& outCoordinates, PRBool& outIsCollapsed) { if (!mPresShell) return NS_ERROR_NOT_INITIALIZED; nsCOMPtr domSelection; nsresult err = mPresShell->GetSelection(getter_AddRefs(domSelection)); if (NS_FAILED(err)) return err; if (!domSelection) return NS_ERROR_NOT_INITIALIZED; // no selection // fill in defaults for failure outCoordinates.x = -1; outCoordinates.y = -1; outIsCollapsed = PR_FALSE; err = domSelection->GetIsCollapsed(SELECTION_NORMAL, &outIsCollapsed); if (NS_FAILED(err)) return err; // code in progress nsCOMPtr focusNode; err = domSelection->GetFocusNode(SELECTION_NORMAL, getter_AddRefs(focusNode)); if (NS_FAILED(err)) return err; if (!focusNode) return NS_ERROR_FAILURE; PRInt32 focusOffset; err = domSelection->GetFocusOffset(SELECTION_NORMAL, &focusOffset); if (NS_FAILED(err)) return err; // is this a text node? nsCOMPtr nodeAsText = do_QueryInterface(focusNode); // note that we only work with text nodes here, unlike when drawing the caret. // this is because this routine is intended for IME support, which only cares about text. if (!nodeAsText) return NS_ERROR_UNEXPECTED; nsCOMPtrcontentNode = do_QueryInterface(focusNode); if (!contentNode) return NS_ERROR_FAILURE; // find the frame that contains the content node that has focus nsIFrame* theFrame = nsnull; err = mPresShell->GetPrimaryFrameFor(contentNode, &theFrame); if (NS_FAILED(err)) return err; if (!theFrame) return NS_ERROR_UNEXPECTED; // find the child frame containing the offset we want PRInt32 contentOffset = focusOffset; err = theFrame->GetChildFrameContainingOffset(focusOffset, &focusOffset, &theFrame); if (NS_FAILED(err)) return err; nsPoint viewOffset(0, 0); nsIView *drawingView; // views are not refcounted GetViewForRendering(theFrame, eTopLevelWindowCoordinates, viewOffset, drawingView); if (!drawingView) return NS_ERROR_UNEXPECTED; // ramp up to make a rendering context for measuring text. // First, we get the pres context ... nsCOMPtr presContext; err = mPresShell->GetPresContext(getter_AddRefs(presContext)); if (NS_FAILED(err)) return err; // ... then get a device context nsCOMPtr dx; err = presContext->GetDeviceContext(getter_AddRefs(dx)); if (NS_FAILED(err)) return err; if (!dx) return NS_ERROR_UNEXPECTED; // ... then tell it to make a rendering context nsCOMPtr rendContext; err = dx->CreateRenderingContext(drawingView, *getter_AddRefs(rendContext)); if (NS_FAILED(err)) return err; if (!rendContext) return NS_ERROR_UNEXPECTED; // now we can measure the offset into the frame. nsPoint framePos(0, 0); theFrame->GetPointFromOffset(presContext, rendContext, contentOffset, &framePos); // now add the frame offset to the view offset, and we're done viewOffset += framePos; outCoordinates = viewOffset; return NS_OK; } //----------------------------------------------------------------------------- NS_IMETHODIMP nsCaret::ClearFrameRefs(nsIFrame* aFrame) { if (mLastCaretFrame == aFrame) { mLastCaretFrame = nsnull; // frames are not refcounted. mLastContentOffset = 0; } // blow away the rendering context, because the frames may well get // reflowed before we draw again NS_IF_RELEASE(mRendContext); // this nulls the ptr // should we just call StopBlinking() here? return NS_OK; } #ifdef XP_MAC #pragma mark - #endif //----------------------------------------------------------------------------- NS_IMETHODIMP nsCaret::NotifySelectionChanged() { if (mVisible) { StopBlinking(); StartBlinking(); } return NS_OK; } #ifdef XP_MAC #pragma mark - #endif //----------------------------------------------------------------------------- void nsCaret::KillTimer() { if (mBlinkTimer) { mBlinkTimer->Cancel(); NS_RELEASE(mBlinkTimer); } } //----------------------------------------------------------------------------- nsresult nsCaret::PrimeTimer() { KillTimer(); // set up the blink timer if (!mReadOnly && mBlinkRate > 0) { nsresult err = NS_NewTimer(&mBlinkTimer); if (NS_FAILED(err)) return err; mBlinkTimer->Init(CaretBlinkCallback, this, mBlinkRate); } return NS_OK; } //----------------------------------------------------------------------------- nsresult nsCaret::StartBlinking() { PrimeTimer(); ToggleDrawnStatus(); DrawCaret(); // draw it right away return NS_OK; } //----------------------------------------------------------------------------- nsresult nsCaret::StopBlinking() { if (mDrawn) // erase the caret if necessary { ToggleDrawnStatus(); DrawCaret(); } // blow away the rendering context, because the frames may well get // reflowed before we draw again NS_IF_RELEASE(mRendContext); // this nulls the ptr KillTimer(); return NS_OK; } //----------------------------------------------------------------------------- // Get the nsIFrame and the content offset for the current caret position. // Returns PR_TRUE if we should go ahead and draw, PR_FALSE otherwise. // PRBool nsCaret::SetupDrawingFrameAndOffset() { if (!mDrawn) { if (!mLastCaretFrame) return PR_FALSE; // check there that our cached offset into the frame content // is still valid. It can go bad when splitting nodes PRInt32 contentStart, contentEnd; mLastCaretFrame->GetOffsets(contentStart, contentEnd); if (mLastContentOffset < contentStart) mLastContentOffset = contentStart; if (mLastContentOffset > contentEnd) mLastContentOffset = contentEnd; return PR_TRUE; } nsCOMPtr domSelection; nsresult err = mPresShell->GetSelection(getter_AddRefs(domSelection)); if (!NS_SUCCEEDED(err) || !domSelection) return PR_FALSE; PRBool isCollapsed; if (domSelection && NS_SUCCEEDED(domSelection->GetIsCollapsed(SELECTION_NORMAL, &isCollapsed)) && isCollapsed) { // start and end parent should be the same since we are collapsed nsCOMPtr focusNode; PRInt32 contentOffset; if (NS_SUCCEEDED(domSelection->GetFocusNode(SELECTION_NORMAL, getter_AddRefs(focusNode))) && focusNode && NS_SUCCEEDED(domSelection->GetFocusOffset(SELECTION_NORMAL, &contentOffset))) { nsCOMPtrcontentNode = do_QueryInterface(focusNode); if (contentNode) { PRBool canContainChildren; // see if we have an offset between child nodes, or an offset into a text // node. if (NS_SUCCEEDED(contentNode->CanContainChildren(canContainChildren)) && canContainChildren) { // point the caret to the start of the child node nsCOMPtr childNode; contentNode->ChildAt(contentOffset, *getter_AddRefs(childNode)); if (childNode) { contentNode = childNode; contentOffset = 0; } } else { //nsCOMPtr nodeAsText = do_QueryInterface(focusNode); //NS_ASSERTION(nodeAsText, "Should have a text node here"); // we can be in a text node, or a BR node here. } nsIFrame* theFrame = nsnull; PRInt32 focusOffset; if (NS_SUCCEEDED(mPresShell->GetPrimaryFrameFor(contentNode, &theFrame)) && theFrame && NS_SUCCEEDED(theFrame->GetChildFrameContainingOffset(contentOffset, &focusOffset, &theFrame))) { // mark the frame, so we get notified on deletion. // frames are never unmarked, which means that we'll touch every frame we visit. // this is not ideal. nsFrameState state; theFrame->GetFrameState(&state); state |= NS_FRAME_EXTERNAL_REFERENCE; theFrame->SetFrameState(state); mLastCaretFrame = theFrame; mLastContentOffset = contentOffset; return PR_TRUE; } } } } return PR_FALSE; } //----------------------------------------------------------------------------- void nsCaret::GetViewForRendering(nsIFrame *caretFrame, EViewCoordinates coordType, nsPoint &viewOffset, nsIView* &outView) { outView = nsnull; nsIView* theView = nsnull; caretFrame->GetOffsetFromView(viewOffset, &theView); if (theView == nsnull) return; nsIView* returnView = nsnull; nscoord x, y; do { theView->GetPosition(&x, &y); viewOffset.x += x; viewOffset.y += y; if (!returnView) { nsCOMPtr viewWidget; theView->GetWidget(*getter_AddRefs(viewWidget)); if (viewWidget) { returnView = theView; if (coordType == eViewCoordinates) break; } } theView->GetParent(theView); } while (theView); outView = returnView; } //----------------------------------------------------------------------------- void nsCaret::DrawCaretWithContext(nsIRenderingContext& inRendContext) { NS_ASSERTION(mLastCaretFrame != nsnull, "Should have a frame here"); nsRect frameRect; mLastCaretFrame->GetRect(frameRect); frameRect.x = 0; // the origin is accounted for in GetViewForRendering() frameRect.y = 0; if (0 == frameRect.height) { frameRect.height = 200; frameRect.y += 200; } nsPoint viewOffset(0, 0); nsIView *drawingView; GetViewForRendering(mLastCaretFrame, eViewCoordinates, viewOffset, drawingView); if (drawingView == nsnull) return; frameRect += viewOffset; inRendContext.PushState(); nsCOMPtr presContext; if (NS_SUCCEEDED(mPresShell->GetPresContext(getter_AddRefs(presContext)))) { nsPoint framePos(0, 0); mLastCaretFrame->GetPointFromOffset(presContext, &inRendContext, mLastContentOffset, &framePos); frameRect += framePos; //printf("Content offset %ld, frame offset %ld\n", focusOffset, framePos.x); frameRect.width = mCaretWidth; mCaretRect = frameRect; if (mDrawn) { if (mReadOnly) inRendContext.SetColor(NS_RGB(85, 85, 85)); // we are drawing it; gray else inRendContext.SetColor(NS_RGB(0, 0, 0)); // we are drawing it; black } else inRendContext.SetColor(NS_RGB(255, 255, 255)); // we are erasing it; white inRendContext.FillRect(mCaretRect); } PRBool dummy; inRendContext.PopState(dummy); } //----------------------------------------------------------------------------- void nsCaret::DrawCaret() { if (! SetupDrawingFrameAndOffset()) return; NS_ASSERTION(mLastCaretFrame != nsnull, "Should have a frame here"); nsPoint viewOffset(0, 0); nsIView *drawingView; GetViewForRendering(mLastCaretFrame, eViewCoordinates, viewOffset, drawingView); if (drawingView) { // make a rendering context for the first view that has a widget if (!mRendContext) { nsCOMPtr presContext; if (NS_SUCCEEDED(mPresShell->GetPresContext(getter_AddRefs(presContext)))) { nsCOMPtr dx; if (NS_SUCCEEDED(presContext->GetDeviceContext(getter_AddRefs(dx))) && dx) { dx->CreateRenderingContext(drawingView, mRendContext); if (!mRendContext) return; } } mRendContext->PushState(); // save the state. We'll pop then push on drawing } DrawCaretWithContext(*mRendContext); NS_RELEASE(mRendContext); } } //----------------------------------------------------------------------------- void nsCaret::RefreshDrawCaret(nsIView *aView, nsIRenderingContext& inRendContext, const nsRect& aDirtyRect) { if (! SetupDrawingFrameAndOffset()) return; NS_ASSERTION(mLastCaretFrame != nsnull, "Should have a frame here"); nsPoint viewOffset(0, 0); nsIView *drawingView; //GetViewForRendering(viewOffset, drawingView); mLastCaretFrame->GetOffsetFromView(viewOffset, &drawingView); // are we in the view that is being painted? if (drawingView == nsnull || drawingView != aView) return; DrawCaretWithContext(inRendContext); } #ifdef XP_MAC #pragma mark - #endif //----------------------------------------------------------------------------- /* static */ void nsCaret::CaretBlinkCallback(nsITimer *aTimer, void *aClosure) { nsCaret *theCaret = NS_REINTERPRET_CAST(nsCaret*, aClosure); if (!theCaret) return; theCaret->ToggleDrawnStatus(); theCaret->DrawCaret(); theCaret->PrimeTimer(); } //----------------------------------------------------------------------------- nsresult NS_NewCaret(nsICaret** aInstancePtrResult) { NS_PRECONDITION(nsnull != aInstancePtrResult, "null ptr"); nsCaret* caret = new nsCaret(); if (nsnull == caret) return NS_ERROR_OUT_OF_MEMORY; return caret->QueryInterface(kICaretID, (void**) aInstancePtrResult); }