/* * Copyright 2007, The Android Open Source Project * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "CachedPrefix.h" #include "android_graphics.h" #include "CachedHistory.h" #include "CachedInput.h" #include "CachedNode.h" #include "SkBitmap.h" #include "SkBounder.h" #include "SkCanvas.h" #include "SkPixelRef.h" #include "SkRegion.h" #include "CachedRoot.h" #ifdef DUMP_NAV_CACHE_USING_PRINTF extern android::Mutex gWriteLogMutex; #endif namespace android { class CommonCheck : public SkBounder { public: enum Type { kNo_Type, kDrawBitmap_Type, kDrawGlyph_Type, kDrawPaint_Type, kDrawPath_Type, kDrawPicture_Type, kDrawPoints_Type, kDrawPosText_Type, kDrawPosTextH_Type, kDrawRect_Type, kDrawSprite_Type, kDrawText_Type, kDrawTextOnPath_Type }; static bool isTextType(Type t) { return t == kDrawPosTextH_Type || t == kDrawText_Type; } CommonCheck() : mType(kNo_Type), mAllOpaque(true), mIsOpaque(true) { setEmpty(); } bool doRect(Type type) { mType = type; return doIRect(mUnion); } bool joinGlyphs(const SkIRect& rect) { bool isGlyph = mType == kDrawGlyph_Type; if (isGlyph) mUnion.join(rect); return isGlyph; } void setAllOpaque(bool opaque) { mAllOpaque = opaque; } void setEmpty() { mUnion.setEmpty(); } void setIsOpaque(bool opaque) { mIsOpaque = opaque; } void setType(Type type) { mType = type; } Type mType; SkIRect mUnion; bool mAllOpaque; bool mIsOpaque; }; #if DEBUG_NAV_UI static const char* TypeNames[] = { "kNo_Type", "kDrawBitmap_Type", "kDrawGlyph_Type", "kDrawPaint_Type", "kDrawPath_Type", "kDrawPicture_Type", "kDrawPoints_Type", "kDrawPosText_Type", "kDrawPosTextH_Type", "kDrawRect_Type", "kDrawSprite_Type", "kDrawText_Type", "kDrawTextOnPath_Type" }; #endif #define kMargin 16 #define kSlop 2 class BoundsCheck : public CommonCheck { public: BoundsCheck() { mAllDrawnIn.setEmpty(); mLastAll.setEmpty(); mLastOver.setEmpty(); } static int Area(SkIRect test) { return test.width() * test.height(); } void checkLast() { if (mAllDrawnIn.isEmpty()) return; if (mLastAll.isEmpty() || Area(mLastAll) < Area(mAllDrawnIn)) { mLastAll = mAllDrawnIn; mDrawnOver.setEmpty(); } mAllDrawnIn.setEmpty(); } bool hidden() { return (mLastAll.isEmpty() && mLastOver.isEmpty()) || mDrawnOver.contains(mBounds); } virtual bool onIRect(const SkIRect& rect) { if (joinGlyphs(rect)) return false; bool interestingType = mType == kDrawBitmap_Type || mType == kDrawRect_Type || isTextType(mType); if (SkIRect::Intersects(mBounds, rect) == false) { DBG_NAV_LOGD("BoundsCheck (no intersect) rect={%d,%d,%d,%d}" " mType=%s", rect.fLeft, rect.fTop, rect.fRight, rect.fBottom, TypeNames[mType]); if (interestingType) checkLast(); return false; } if (interestingType == false) return false; if (mBoundsSlop.contains(rect) || (mBounds.fLeft == rect.fLeft && mBounds.fRight == rect.fRight && mBounds.fTop >= rect.fTop && mBounds.fBottom <= rect.fBottom) || (mBounds.fTop == rect.fTop && mBounds.fBottom == rect.fBottom && mBounds.fLeft >= rect.fLeft && mBounds.fRight <= rect.fRight)) { mDrawnOver.setEmpty(); mAllDrawnIn.join(rect); DBG_NAV_LOGD("BoundsCheck (contains) rect={%d,%d,%d,%d}" " mAllDrawnIn={%d,%d,%d,%d} mType=%s", rect.fLeft, rect.fTop, rect.fRight, rect.fBottom, mAllDrawnIn.fLeft, mAllDrawnIn.fTop, mAllDrawnIn.fRight, mAllDrawnIn.fBottom, TypeNames[mType]); } else { checkLast(); if (!isTextType(mType)) { if ( #if 0 // should the opaqueness of the bitmap disallow its ability to draw over? // not sure that this test is needed (mType != kDrawBitmap_Type || (mIsOpaque && mAllOpaque)) && #endif mLastAll.isEmpty() == false) mDrawnOver.op(rect, SkRegion::kUnion_Op); } else { // FIXME // sometimes the text is not drawn entirely inside the cursor area, even though // it is the correct text. Until I figure out why, I allow text drawn at the // end that is not covered up by something else to represent the link // example that triggers this that should be figured out: // http://cdn.labpixies.com/campaigns/blackjack/blackjack.html?lang=en&country=US&libs=assets/feature/core // ( http://tinyurl.com/ywsyzb ) mLastOver = rect; } #if DEBUG_NAV_UI const SkIRect& drawnOver = mDrawnOver.getBounds(); DBG_NAV_LOGD("(overlaps) rect={%d,%d,%d,%d}" " mDrawnOver={%d,%d,%d,%d} mType=%s mIsOpaque=%s mAllOpaque=%s", rect.fLeft, rect.fTop, rect.fRight, rect.fBottom, drawnOver.fLeft, drawnOver.fTop, drawnOver.fRight, drawnOver.fBottom, TypeNames[mType], mIsOpaque ? "true" : "false", mAllOpaque ? "true" : "false"); #endif } return false; } SkIRect mBounds; SkIRect mBoundsSlop; SkRegion mDrawnOver; SkIRect mLastOver; SkIRect mAllDrawnIn; SkIRect mLastAll; }; class BoundsCanvas : public SkCanvas { public: BoundsCanvas(CommonCheck* bounder) : mBounder(*bounder) { mTransparentLayer = 0; setBounder(bounder); } virtual ~BoundsCanvas() { setBounder(NULL); } virtual void drawPaint(const SkPaint& paint) { mBounder.setType(CommonCheck::kDrawPaint_Type); SkCanvas::drawPaint(paint); } virtual void drawPoints(PointMode mode, size_t count, const SkPoint pts[], const SkPaint& paint) { mBounder.setType(CommonCheck::kDrawPoints_Type); SkCanvas::drawPoints(mode, count, pts, paint); } virtual void drawRect(const SkRect& rect, const SkPaint& paint) { mBounder.setType(CommonCheck::kDrawRect_Type); SkCanvas::drawRect(rect, paint); } virtual void drawPath(const SkPath& path, const SkPaint& paint) { mBounder.setType(CommonCheck::kDrawPath_Type); SkCanvas::drawPath(path, paint); } virtual void commonDrawBitmap(const SkBitmap& bitmap, const SkMatrix& matrix, const SkPaint& paint) { mBounder.setType(CommonCheck::kDrawBitmap_Type); mBounder.setIsOpaque(bitmap.isOpaque()); SkCanvas::commonDrawBitmap(bitmap, matrix, paint); } virtual void drawSprite(const SkBitmap& bitmap, int left, int top, const SkPaint* paint = NULL) { mBounder.setType(CommonCheck::kDrawSprite_Type); mBounder.setIsOpaque(bitmap.isOpaque()); SkCanvas::drawSprite(bitmap, left, top, paint); } virtual void drawText(const void* text, size_t byteLength, SkScalar x, SkScalar y, const SkPaint& paint) { mBounder.setEmpty(); mBounder.setType(CommonCheck::kDrawGlyph_Type); SkCanvas::drawText(text, byteLength, x, y, paint); mBounder.doRect(CommonCheck::kDrawText_Type); } virtual void drawPosText(const void* text, size_t byteLength, const SkPoint pos[], const SkPaint& paint) { mBounder.setEmpty(); mBounder.setType(CommonCheck::kDrawGlyph_Type); SkCanvas::drawPosText(text, byteLength, pos, paint); mBounder.doRect(CommonCheck::kDrawPosText_Type); } virtual void drawPosTextH(const void* text, size_t byteLength, const SkScalar xpos[], SkScalar constY, const SkPaint& paint) { mBounder.setEmpty(); mBounder.setType(CommonCheck::kDrawGlyph_Type); SkCanvas::drawPosTextH(text, byteLength, xpos, constY, paint); if (mBounder.mUnion.isEmpty()) return; SkPaint::FontMetrics metrics; paint.getFontMetrics(&metrics); SkPoint upDown[2] = { {xpos[0], constY + metrics.fAscent}, {xpos[0], constY + metrics.fDescent} }; const SkMatrix& matrix = getTotalMatrix(); matrix.mapPoints(upDown, 2); if (upDown[0].fX == upDown[1].fX) { mBounder.mUnion.fTop = SkScalarFloor(upDown[0].fY); mBounder.mUnion.fBottom = SkScalarFloor(upDown[1].fY); } mBounder.doRect(CommonCheck::kDrawPosTextH_Type); } virtual void drawTextOnPath(const void* text, size_t byteLength, const SkPath& path, const SkMatrix* matrix, const SkPaint& paint) { mBounder.setEmpty(); mBounder.setType(CommonCheck::kDrawGlyph_Type); SkCanvas::drawTextOnPath(text, byteLength, path, matrix, paint); mBounder.doRect(CommonCheck::kDrawTextOnPath_Type); } virtual void drawPicture(SkPicture& picture) { mBounder.setType(CommonCheck::kDrawPicture_Type); SkCanvas::drawPicture(picture); } virtual int saveLayer(const SkRect* bounds, const SkPaint* paint, SaveFlags flags) { int depth = SkCanvas::saveLayer(bounds, paint, flags); if (mTransparentLayer == 0 && paint && paint->getAlpha() < 255) { mTransparentLayer = depth; mBounder.setAllOpaque(false); } return depth; } virtual void restore() { int depth = getSaveCount(); if (depth == mTransparentLayer) { mTransparentLayer = 0; mBounder.setAllOpaque(true); } SkCanvas::restore(); } int mTransparentLayer; CommonCheck& mBounder; }; /* LeftCheck examines the text in a picture, within a viewable rectangle, and returns via left() the position of the left edge of the paragraph. It first looks at the left edge of the test point, then looks above and below it for more lines of text to determine the div's left edge. */ class LeftCheck : public CommonCheck { public: LeftCheck(int x, int y) : mX(x), mY(y), mHitLeft(INT_MAX), mMostLeft(INT_MAX) { mHit.set(x - (HIT_SLOP << 1), y - HIT_SLOP, x, y + HIT_SLOP); mPartial.setEmpty(); mBounds.setEmpty(); mPartialType = kNo_Type; } int left() { if (isTextType(mType)) doRect(); // process the final line of text return mMostLeft != INT_MAX ? mMostLeft : mX >> 1; } // FIXME: this is identical to CenterCheck::onIRect() // refactor so that LeftCheck and CenterCheck inherit common functions virtual bool onIRect(const SkIRect& rect) { bool opaqueBitmap = mType == kDrawBitmap_Type && mIsOpaque; if (opaqueBitmap && rect.contains(mX, mY)) { mMostLeft = rect.fLeft; return false; } if (joinGlyphs(rect)) // assembles glyphs into a text string return false; if (!isTextType(mType) && !opaqueBitmap) return false; /* Text on one line may be broken into several parts. Reassemble the text into a rectangle before considering it. */ if (rect.fTop < mPartial.fBottom && rect.fBottom > mPartial.fTop && mPartial.fRight + SLOP >= rect.fLeft && (mPartialType != kDrawBitmap_Type || mPartial.height() <= rect.height() + HIT_SLOP)) { DBG_NAV_LOGD("LeftCheck join mPartial=(%d, %d, %d, %d)" " rect=(%d, %d, %d, %d)", mPartial.fLeft, mPartial.fTop, mPartial.fRight, mPartial.fBottom, rect.fLeft, rect.fTop, rect.fRight, rect.fBottom); mPartial.join(rect); return false; } if (mPartial.isEmpty() == false) { doRect(); // process the previous line of text #if DEBUG_NAV_UI if (mHitLeft == INT_MAX) DBG_NAV_LOGD("LeftCheck disabled rect=(%d, %d, %d, %d)", rect.fLeft, rect.fTop, rect.fRight, rect.fBottom); #endif } mPartial = rect; mPartialType = mType; return false; } void doRect() { /* Record the outer bounds of the lines of text that intersect the touch coordinates, given some slop */ if (SkIRect::Intersects(mPartial, mHit)) { if (mHitLeft > mPartial.fLeft) mHitLeft = mPartial.fLeft; DBG_NAV_LOGD("LeftCheck mHitLeft=%d", mHitLeft); } else if (mHitLeft == INT_MAX) return; // wait for intersect success /* If text is too far away vertically, don't consider it */ if (!mBounds.isEmpty() && (mPartial.fTop > mBounds.fBottom + SLOP || mPartial.fBottom < mBounds.fTop - SLOP)) { DBG_NAV_LOGD("LeftCheck stop mPartial=(%d, %d, %d, %d)" " mBounds=(%d, %d, %d, %d)", mPartial.fLeft, mPartial.fTop, mPartial.fRight, mPartial.fBottom, mBounds.fLeft, mBounds.fTop, mBounds.fRight, mBounds.fBottom); mHitLeft = INT_MAX; // and disable future comparisons return; } /* If the considered text is completely to the left or right of the touch coordinates, skip it, turn off further detection */ if (mPartial.fLeft > mX || mPartial.fRight < mX) { DBG_NAV_LOGD("LeftCheck stop mX=%d mPartial=(%d, %d, %d, %d)", mX, mPartial.fLeft, mPartial.fTop, mPartial.fRight, mPartial.fBottom); mHitLeft = INT_MAX; return; } /* record the smallest margins on the left and right */ if (mMostLeft > mPartial.fLeft) { DBG_NAV_LOGD("LeftCheck new mMostLeft=%d (old=%d)", mPartial.fLeft, mMostLeft); mMostLeft = mPartial.fLeft; } if (mBounds.isEmpty()) mBounds = mPartial; else if (mPartial.fBottom > mBounds.fBottom) { DBG_NAV_LOGD("LeftCheck new bottom=%d (old=%d)", mPartial.fBottom, mBounds.fBottom); mBounds.fBottom = mPartial.fBottom; } } static const int HIT_SLOP = 5; // space between text parts and lines static const int SLOP = 30; // space between text parts and lines /* const */ SkIRect mHit; // sloppy hit rectangle SkIRect mBounds; // reference bounds SkIRect mPartial; // accumulated text bounds, per line const int mX; // touch location const int mY; int mHitLeft; // touched text extremes int mMostLeft; // paragraph extremes Type mPartialType; }; /* CenterCheck examines the text in a picture, within a viewable rectangle, and returns via center() the optimal amount to scroll in x to display the paragraph of text. The caller of CenterCheck has configured (but not allocated) a bitmap the height and three times the width of the view. The picture is drawn centered in the bitmap, so text that would be revealed, if the view was scrolled up to a view-width to the left or right, is considered. */ class CenterCheck : public CommonCheck { public: CenterCheck(int x, int y, int width) : mX(x), mY(y), mHitLeft(x), mHitRight(x), mMostLeft(INT_MAX), mMostRight(-INT_MAX), mViewLeft(width), mViewRight(width << 1) { mHit.set(x - CENTER_SLOP, y - CENTER_SLOP, x + CENTER_SLOP, y + CENTER_SLOP); mPartial.setEmpty(); } int center() { doRect(); // process the final line of text /* If the touch coordinates aren't near any text, return 0 */ if (mHitLeft == mHitRight) { DBG_NAV_LOGD("abort: mHitLeft=%d ==mHitRight", mHitLeft); return 0; } int leftOver = mHitLeft - mViewLeft; int rightOver = mHitRight - mViewRight; int center; /* If the touched text is too large to entirely fit on the screen, center it. */ if (leftOver < 0 && rightOver > 0) { center = (leftOver + rightOver) >> 1; DBG_NAV_LOGD("overlap: leftOver=%d rightOver=%d center=%d", leftOver, rightOver, center); return center; } center = (mMostLeft + mMostRight) >> 1; // the paragraph center if (leftOver > 0 && rightOver >= 0) { // off to the right if (center > mMostLeft) // move to center loses left-most text? center = mMostLeft; } else if (rightOver < 0 && leftOver <= 0) { // off to the left if (center < mMostRight) // move to center loses right-most text? center = mMostRight; } else { #ifdef DONT_CENTER_IF_ALREADY_VISIBLE center = 0; // paragraph is already fully visible #endif } DBG_NAV_LOGD("scroll: leftOver=%d rightOver=%d center=%d", leftOver, rightOver, center); return center; } protected: virtual bool onIRect(const SkIRect& rect) { if (joinGlyphs(rect)) // assembles glyphs into a text string return false; if (!isTextType(mType)) return false; /* Text on one line may be broken into several parts. Reassemble the text into a rectangle before considering it. */ if (rect.fTop < mPartial.fBottom && rect.fBottom > mPartial.fTop && mPartial.fRight + CENTER_SLOP >= rect.fLeft) { DBG_NAV_LOGD("join mPartial=(%d, %d, %d, %d) rect=(%d, %d, %d, %d)", mPartial.fLeft, mPartial.fTop, mPartial.fRight, mPartial.fBottom, rect.fLeft, rect.fTop, rect.fRight, rect.fBottom); mPartial.join(rect); return false; } if (mPartial.isEmpty() == false) doRect(); // process the previous line of text mPartial = rect; return false; } void doRect() { /* Record the outer bounds of the lines of text that was 'hit' by the touch coordinates, given some slop */ if (SkIRect::Intersects(mPartial, mHit)) { if (mHitLeft > mPartial.fLeft) mHitLeft = mPartial.fLeft; if (mHitRight < mPartial.fRight) mHitRight = mPartial.fRight; DBG_NAV_LOGD("mHitLeft=%d mHitRight=%d", mHitLeft, mHitRight); } /* If the considered text is completely to the left or right of the touch coordinates, skip it */ if (mPartial.fLeft > mX || mPartial.fRight < mX) return; int leftOver = mPartial.fLeft - mViewLeft; int rightOver = mPartial.fRight - mViewRight; /* If leftOver <= 0, the text starts off the screen. If rightOver >= 0, the text ends off the screen. */ if (leftOver <= 0 && rightOver >= 0) // discard wider than screen return; #ifdef DONT_CENTER_IF_ALREADY_VISIBLE if (leftOver > 0 && rightOver < 0) // discard already visible return; #endif /* record the smallest margins on the left and right */ if (mMostLeft > leftOver) mMostLeft = leftOver; if (mMostRight < rightOver) mMostRight = rightOver; DBG_NAV_LOGD("leftOver=%d rightOver=%d mMostLeft=%d mMostRight=%d", leftOver, rightOver, mMostLeft, mMostRight); } static const int CENTER_SLOP = 10; // space between text parts and lines /* const */ SkIRect mHit; // sloppy hit rectangle SkIRect mPartial; // accumulated text bounds, per line const int mX; // touch location const int mY; int mHitLeft; // touched text extremes int mHitRight; int mMostLeft; // paragraph extremes int mMostRight; const int mViewLeft; // middle third of 3x-wide view const int mViewRight; }; class ImageCanvas : public SkCanvas { public: ImageCanvas(SkBounder* bounder) : mURI(NULL) { setBounder(bounder); } // Currently webkit's bitmap draws always seem to be cull'd before this entry // point is called, so we assume that any bitmap that gets here is inside our // tiny clip (may not be true in the future) virtual void commonDrawBitmap(const SkBitmap& bitmap, const SkMatrix& , const SkPaint& ) { SkPixelRef* pixelRef = bitmap.pixelRef(); if (pixelRef != NULL) { mURI = pixelRef->getURI(); } } const char* mURI; }; class ImageCheck : public SkBounder { public: virtual bool onIRect(const SkIRect& rect) { return false; } }; class JiggleCheck : public CommonCheck { public: JiggleCheck(int delta, int width) : mDelta(delta), mMaxX(width) { mMaxJiggle = 0; mMinX = mMinJiggle = abs(delta); mMaxWidth = width + mMinX; } int jiggle() { if (mMinJiggle > mMaxJiggle) return mDelta; int avg = (mMinJiggle + mMaxJiggle + 1) >> 1; return mDelta < 0 ? -avg : avg; } virtual bool onIRect(const SkIRect& rect) { if (joinGlyphs(rect)) return false; if (mType != kDrawBitmap_Type && !isTextType(mType)) return false; int min, max; if (mDelta < 0) { min = mMinX - rect.fLeft; max = mMaxWidth - rect.fRight; } else { min = rect.fRight - mMaxX; max = rect.fLeft; } if (min <= 0) return false; if (max >= mMinX) return false; if (mMinJiggle > min) mMinJiggle = min; if (mMaxJiggle < max) mMaxJiggle = max; return false; } int mDelta; int mMaxJiggle; int mMaxX; int mMinJiggle; int mMinX; int mMaxWidth; }; class RingCheck : public CommonCheck { public: RingCheck(const WTF::Vector& rings, const WebCore::IntPoint& location) : mSuccess(true) { const WebCore::IntRect* r; for (r = rings.begin(); r != rings.end(); r++) { SkIRect fatter = {r->x(), r->y(), r->right(), r->bottom()}; fatter.inset(-CURSOR_RING_HIT_TEST_RADIUS, -CURSOR_RING_HIT_TEST_RADIUS); DBG_NAV_LOGD("fat=(%d,%d,r=%d,b=%d)", fatter.fLeft, fatter.fTop, fatter.fRight, fatter.fBottom); mRings.op(fatter, SkRegion::kUnion_Op); } DBG_NAV_LOGD("translate=(%d,%d)", -location.x(), -location.y()); mRings.translate(-location.x(), -location.y()); } virtual bool onIRect(const SkIRect& rect) { if (mSuccess && mType == kDrawGlyph_Type) { DBG_NAV_LOGD("contains (%d,%d,r=%d,b=%d) == %s", rect.fLeft, rect.fTop, rect.fRight, rect.fBottom, mRings.contains(rect) ? "true" : "false"); mSuccess &= mRings.contains(rect); } return false; } bool success() { return mSuccess; } SkRegion mRings; bool mSuccess; }; bool CachedRoot::adjustForScroll(BestData* best, CachedFrame::Direction direction, WebCore::IntPoint* scrollPtr, bool findClosest) { WebCore::IntRect newOutset; const CachedNode* newNode = best->mNode; // see if there's a middle node // if the middle node is in the visited list, // or if none was computed and the newNode is in the visited list, // treat result as NULL if (newNode != NULL && findClosest) { if (best->bounds().intersects(mHistory->mPriorBounds) == false && checkBetween(best, direction)) newNode = best->mNode; if (findClosest && maskIfHidden(best)) { innerMove(document(), best, direction, scrollPtr, false); return true; } newNode->cursorRingBounds(&newOutset); } int delta; bool newNodeInView = scrollDelta(newOutset, direction, &delta); if (delta && scrollPtr && (newNode == NULL || newNodeInView == false || (best->mNavOutside && best->mWorkingOutside))) *scrollPtr = WebCore::IntPoint(direction & UP_DOWN ? 0 : delta, direction & UP_DOWN ? delta : 0); return false; } int CachedRoot::checkForCenter(int x, int y) const { int width = mViewBounds.width(); CenterCheck centerCheck(x + width - mViewBounds.x(), y - mViewBounds.y(), width); BoundsCanvas checker(¢erCheck); SkBitmap bitmap; bitmap.setConfig(SkBitmap::kARGB_8888_Config, width * 3, mViewBounds.height()); checker.setBitmapDevice(bitmap); checker.translate(SkIntToScalar(width - mViewBounds.x()), SkIntToScalar(-mViewBounds.y())); checker.drawPicture(*mPicture); return centerCheck.center(); } void CachedRoot::checkForJiggle(int* xDeltaPtr) const { int xDelta = *xDeltaPtr; JiggleCheck jiggleCheck(xDelta, mViewBounds.width()); BoundsCanvas checker(&jiggleCheck); SkBitmap bitmap; int absDelta = abs(xDelta); bitmap.setConfig(SkBitmap::kARGB_8888_Config, mViewBounds.width() + absDelta, mViewBounds.height()); checker.setBitmapDevice(bitmap); checker.translate(SkIntToScalar(-mViewBounds.x() - (xDelta < 0 ? xDelta : 0)), SkIntToScalar(-mViewBounds.y())); checker.drawPicture(*mPicture); *xDeltaPtr = jiggleCheck.jiggle(); } bool CachedRoot::checkRings(const WTF::Vector& rings, const WebCore::IntRect& bounds) const { if (!mPicture) return false; RingCheck ringCheck(rings, bounds.location()); BoundsCanvas checker(&ringCheck); SkBitmap bitmap; bitmap.setConfig(SkBitmap::kARGB_8888_Config, bounds.width(), bounds.height()); checker.setBitmapDevice(bitmap); checker.translate(SkIntToScalar(-bounds.x()), SkIntToScalar(-bounds.y())); checker.drawPicture(*mPicture); DBG_NAV_LOGD("bounds=(%d,%d,r=%d,b=%d) success=%s", bounds.x(), bounds.y(), bounds.right(), bounds.bottom(), ringCheck.success() ? "true" : "false"); return ringCheck.success(); } CachedRoot::ImeAction CachedRoot::currentTextFieldAction() const { const CachedFrame* currentFrame; const CachedNode* current = currentCursor(¤tFrame); if (!current) { // Although the cursor is not on a textfield, a textfield may have // focus. Find the action for that textfield. current = currentFocus(¤tFrame); if (!current) // Error case. No cursor and no focus. return FAILURE; } const CachedNode* firstTextfield = nextTextField(0, 0, false); if (!firstTextfield) { // Error case. There are no textfields in this tree. return FAILURE; } // Now find the next textfield/area starting with the cursor const CachedFrame* potentialFrame; const CachedNode* potentialNext = currentFrame->nextTextField(current, &potentialFrame, true); if (potentialNext && currentFrame->textInput(current)->formPointer() == potentialFrame->textInput(potentialNext)->formPointer()) { // There is a textfield/area after the cursor in the same form, // so the textfield under the cursor should have the NEXT action return NEXT; } // If this line is reached, we know that the textfield under the cursor is // the last one. Make it GO to allow a submit return GO; } const CachedNode* CachedRoot::findAt(const WebCore::IntRect& rect, const CachedFrame** framePtr, int* x, int* y, bool checkForHidden) const { int best = INT_MAX; bool inside = false; (const_cast(this))->resetClippedOut(); const CachedNode* directHit = NULL; const CachedNode* node = findBestAt(rect, &best, &inside, &directHit, framePtr, x, y, checkForHidden); DBG_NAV_LOGD("node=%d (%p)", node == NULL ? 0 : node->index(), node == NULL ? NULL : node->nodePointer()); if (node == NULL) { node = findBestHitAt(rect, framePtr, x, y); DBG_NAV_LOGD("node=%d (%p)", node == NULL ? 0 : node->index(), node == NULL ? NULL : node->nodePointer()); } if (node == NULL) { *framePtr = findBestFrameAt(rect.x() + (rect.width() >> 1), rect.y() + (rect.height() >> 1)); } return node; } WebCore::IntPoint CachedRoot::cursorLocation() const { const WebCore::IntRect& bounds = mHistory->mNavBounds; return WebCore::IntPoint(bounds.x() + (bounds.width() >> 1), bounds.y() + (bounds.height() >> 1)); } WebCore::IntPoint CachedRoot::focusLocation() const { return WebCore::IntPoint(mFocusBounds.x() + (mFocusBounds.width() >> 1), mFocusBounds.y() + (mFocusBounds.height() >> 1)); } // These reset the values because we only want to get the selection the first time. // After that, the selection is no longer accurate. int CachedRoot::getAndResetSelectionEnd() { int end = mSelectionEnd; mSelectionEnd = -1; return end; } int CachedRoot::getAndResetSelectionStart() { int start = mSelectionStart; mSelectionStart = -1; return start; } int CachedRoot::getBlockLeftEdge(int x, int y, float scale) const { DBG_NAV_LOGD("x=%d y=%d scale=%g mViewBounds=(%d,%d,%d,%d)", x, y, scale, mViewBounds.x(), mViewBounds.y(), mViewBounds.width(), mViewBounds.height()); // if (x, y) is in a textArea or textField, return that const int slop = 1; WebCore::IntRect rect = WebCore::IntRect(x - slop, y - slop, slop * 2, slop * 2); const CachedFrame* frame; int fx, fy; const CachedNode* node = findAt(rect, &frame, &fx, &fy, true); if (node && node->wantsKeyEvents()) { DBG_NAV_LOGD("x=%d (%s)", node->bounds().x(), node->isTextInput() ? "text" : "plugin"); return node->bounds().x(); } int halfW = (int) (mViewBounds.width() * scale * 0.5f); int fullW = halfW << 1; int halfH = (int) (mViewBounds.height() * scale * 0.5f); int fullH = halfH << 1; LeftCheck leftCheck(fullW, halfH); BoundsCanvas checker(&leftCheck); SkBitmap bitmap; bitmap.setConfig(SkBitmap::kARGB_8888_Config, fullW, fullH); checker.setBitmapDevice(bitmap); checker.translate(SkIntToScalar(fullW - x), SkIntToScalar(halfH - y)); checker.drawPicture(*mPicture); int result = x + leftCheck.left() - fullW; DBG_NAV_LOGD("halfW=%d halfH=%d mMostLeft=%d x=%d", halfW, halfH, leftCheck.mMostLeft, result); return result; } void CachedRoot::getSimulatedMousePosition(WebCore::IntPoint* point) const { #ifndef NDEBUG ASSERT(CachedFrame::mDebug.mInUse); #endif const WebCore::IntRect& mouseBounds = mHistory->mMouseBounds; int x = mouseBounds.x(); int y = mouseBounds.y(); int width = mouseBounds.width(); int height = mouseBounds.height(); point->setX(x + (width >> 1)); // default to box center point->setY(y + (height >> 1)); #if DEBUG_NAV_UI const WebCore::IntRect& navBounds = mHistory->mNavBounds; DBG_NAV_LOGD("mHistory->mNavBounds={%d,%d,%d,%d} " "mHistory->mMouseBounds={%d,%d,%d,%d} point={%d,%d}", navBounds.x(), navBounds.y(), navBounds.width(), navBounds.height(), mouseBounds.x(), mouseBounds.y(), mouseBounds.width(), mouseBounds.height(), point->x(), point->y()); #endif } void CachedRoot::init(WebCore::Frame* frame, CachedHistory* history) { CachedFrame::init(this, -1, frame); reset(); mHistory = history; mPicture = NULL; } bool CachedRoot::innerDown(const CachedNode* test, BestData* bestData) const { ASSERT(minWorkingVertical() >= mViewBounds.x()); ASSERT(maxWorkingVertical() <= mViewBounds.right()); setupScrolledBounds(); // (line up) mScrolledBounds.setHeight(mScrolledBounds.height() + mMaxYScroll); int testTop = mScrolledBounds.y(); int viewBottom = mViewBounds.bottom(); const WebCore::IntRect& navBounds = mHistory->mNavBounds; if (navBounds.isEmpty() == false && navBounds.bottom() > viewBottom && viewBottom < mContents.height()) return false; if (navBounds.isEmpty() == false) { int navTop = navBounds.y(); int scrollBottom; if (testTop < navTop && navTop < (scrollBottom = mScrolledBounds.bottom())) { mScrolledBounds.setHeight(scrollBottom - navTop); mScrolledBounds.setY(navTop); } } frameDown(test, NULL, bestData, currentCursor()); return true; } bool CachedRoot::innerLeft(const CachedNode* test, BestData* bestData) const { ASSERT(minWorkingHorizontal() >= mViewBounds.y()); ASSERT(maxWorkingHorizontal() <= mViewBounds.bottom()); setupScrolledBounds(); mScrolledBounds.setX(mScrolledBounds.x() - mMaxXScroll); mScrolledBounds.setWidth(mScrolledBounds.width() + mMaxXScroll); int testRight = mScrolledBounds.right(); int viewLeft = mViewBounds.x(); const WebCore::IntRect& navBounds = mHistory->mNavBounds; if (navBounds.isEmpty() == false && navBounds.x() < viewLeft && viewLeft > mContents.x()) return false; if (navBounds.isEmpty() == false) { int navRight = navBounds.right(); int scrollLeft; if (testRight > navRight && navRight > (scrollLeft = mScrolledBounds.x())) mScrolledBounds.setWidth(navRight - scrollLeft); } frameLeft(test, NULL, bestData, currentCursor()); return true; } void CachedRoot::innerMove(const CachedNode* node, BestData* bestData, Direction direction, WebCore::IntPoint* scroll, bool firstCall) { bestData->reset(); bool outOfCursor = mCursorIndex == CURSOR_CLEARED; DBG_NAV_LOGD("mHistory->didFirstLayout()=%s && mCursorIndex=%d", mHistory->didFirstLayout() ? "true" : "false", mCursorIndex); if (mHistory->didFirstLayout() && mCursorIndex < CURSOR_SET) { mHistory->reset(); outOfCursor = true; } const CachedNode* cursor = currentCursor(); mHistory->setWorking(direction, cursor, mViewBounds); bool findClosest = false; if (mScrollOnly == false) { switch (direction) { case LEFT: if (outOfCursor) mHistory->mNavBounds = WebCore::IntRect(mViewBounds.right(), mViewBounds.y(), 1, mViewBounds.height()); findClosest = innerLeft(node, bestData); break; case RIGHT: if (outOfCursor) mHistory->mNavBounds = WebCore::IntRect(mViewBounds.x() - 1, mViewBounds.y(), 1, mViewBounds.height()); findClosest = innerRight(node, bestData); break; case UP: if (outOfCursor) mHistory->mNavBounds = WebCore::IntRect(mViewBounds.x(), mViewBounds.bottom(), mViewBounds.width(), 1); findClosest = innerUp(node, bestData); break; case DOWN: if (outOfCursor) mHistory->mNavBounds = WebCore::IntRect(mViewBounds.x(), mViewBounds.y() - 1, mViewBounds.width(), 1); findClosest = innerDown(node, bestData); break; case UNINITIALIZED: default: ASSERT(0); } } if (firstCall) mHistory->mPriorBounds = mHistory->mNavBounds; // bounds always advances, even if new node is ultimately NULL bestData->mMouseBounds = bestData->mNodeBounds; if (adjustForScroll(bestData, direction, scroll, findClosest)) return; if (bestData->mNode != NULL) { mHistory->addToVisited(bestData->mNode, direction); mHistory->mNavBounds = bestData->mNodeBounds; mHistory->mMouseBounds = bestData->mMouseBounds; } else if (scroll->x() != 0 || scroll->y() != 0) { WebCore::IntRect newBounds = mHistory->mNavBounds; int offsetX = scroll->x(); int offsetY = scroll->y(); newBounds.move(offsetX, offsetY); if (mViewBounds.x() > newBounds.x()) offsetX = mViewBounds.x() - mHistory->mNavBounds.x(); else if (mViewBounds.right() < newBounds.right()) offsetX = mViewBounds.right() - mHistory->mNavBounds.right(); if (mViewBounds.y() > newBounds.y()) offsetY = mViewBounds.y() - mHistory->mNavBounds.y(); else if (mViewBounds.bottom() < newBounds.bottom()) offsetY = mViewBounds.bottom() - mHistory->mNavBounds.bottom(); mHistory->mNavBounds.move(offsetX, offsetY); } mHistory->setDidFirstLayout(false); } bool CachedRoot::innerRight(const CachedNode* test, BestData* bestData) const { ASSERT(minWorkingHorizontal() >= mViewBounds.y()); ASSERT(maxWorkingHorizontal() <= mViewBounds.bottom()); setupScrolledBounds(); // (align) mScrolledBounds.setWidth(mScrolledBounds.width() + mMaxXScroll); int testLeft = mScrolledBounds.x(); int viewRight = mViewBounds.right(); const WebCore::IntRect& navBounds = mHistory->mNavBounds; if (navBounds.isEmpty() == false && navBounds.right() > viewRight && viewRight < mContents.width()) return false; if (navBounds.isEmpty() == false) { int navLeft = navBounds.x(); int scrollRight; if (testLeft < navLeft && navLeft < (scrollRight = mScrolledBounds.right())) { mScrolledBounds.setWidth(scrollRight - navLeft); mScrolledBounds.setX(navLeft); } } frameRight(test, NULL, bestData, currentCursor()); return true; } bool CachedRoot::innerUp(const CachedNode* test, BestData* bestData) const { ASSERT(minWorkingVertical() >= mViewBounds.x()); ASSERT(maxWorkingVertical() <= mViewBounds.right()); setupScrolledBounds(); mScrolledBounds.setY(mScrolledBounds.y() - mMaxYScroll); mScrolledBounds.setHeight(mScrolledBounds.height() + mMaxYScroll); int testBottom = mScrolledBounds.bottom(); int viewTop = mViewBounds.y(); const WebCore::IntRect& navBounds = mHistory->mNavBounds; if (navBounds.isEmpty() == false && navBounds.y() < viewTop && viewTop > mContents.y()) return false; if (navBounds.isEmpty() == false) { int navBottom = navBounds.bottom(); int scrollTop; if (testBottom > navBottom && navBottom > (scrollTop = mScrolledBounds.y())) mScrolledBounds.setHeight(navBottom - scrollTop); } frameUp(test, NULL, bestData, currentCursor()); return true; } WebCore::String CachedRoot::imageURI(int x, int y) const { ImageCheck imageCheck; ImageCanvas checker(&imageCheck); SkBitmap bitmap; bitmap.setConfig(SkBitmap::kARGB_8888_Config, 1, 1); checker.setBitmapDevice(bitmap); checker.translate(SkIntToScalar(-x), SkIntToScalar(-y)); checker.drawPicture(*mPicture); return WebCore::String(checker.mURI); } bool CachedRoot::maskIfHidden(BestData* best) const { if (mPicture == NULL) { DBG_NAV_LOG("missing picture"); return false; } const CachedNode* bestNode = best->mNode; if (bestNode->isUnclipped()) return false; // given the picture matching this nav cache // create an SkBitmap with dimensions of the cursor intersected w/ extended view const WebCore::IntRect& nodeBounds = bestNode->getBounds(); WebCore::IntRect bounds = nodeBounds; bounds.intersect(mScrolledBounds); int leftMargin = bounds.x() == nodeBounds.x() ? kMargin : 0; int topMargin = bounds.y() == nodeBounds.y() ? kMargin : 0; int rightMargin = bounds.right() == nodeBounds.right() ? kMargin : 0; int bottomMargin = bounds.bottom() == nodeBounds.bottom() ? kMargin : 0; bool unclipped = (leftMargin & topMargin & rightMargin & bottomMargin) != 0; WebCore::IntRect marginBounds = nodeBounds; marginBounds.inflate(kMargin); marginBounds.intersect(mScrolledBounds); BoundsCheck boundsCheck; BoundsCanvas checker(&boundsCheck); boundsCheck.mBounds.set(leftMargin, topMargin, leftMargin + bounds.width(), topMargin + bounds.height()); boundsCheck.mBoundsSlop = boundsCheck.mBounds; boundsCheck.mBoundsSlop.inset(-kSlop, -kSlop); SkBitmap bitmap; bitmap.setConfig(SkBitmap::kARGB_8888_Config, marginBounds.width(), marginBounds.height()); checker.setBitmapDevice(bitmap); // insert probes to be called when the data corresponding to this ring is drawn // need to know if ring was generated by text, image, or parent (like div) // ? need to know (like imdb menu bar) to give up sometimes (when?) checker.translate(SkIntToScalar(leftMargin - bounds.x()), SkIntToScalar(topMargin - bounds.y())); checker.drawPicture(*mPicture); boundsCheck.checkLast(); // was it not drawn or clipped out? if (boundsCheck.hidden()) { // if hidden, return false so that nav can try again CachedNode* node = const_cast(best->mNode); #if DEBUG_NAV_UI const SkIRect& m = boundsCheck.mBounds; const SkIRect& s = boundsCheck.mBoundsSlop; DBG_NAV_LOGD("hidden node:%p (%d) mBounds={%d,%d,%d,%d} mBoundsSlop=" "{%d,%d,%d,%d}", node, node->index(), m.fLeft, m.fTop, m.fRight, m.fBottom, s.fLeft, s.fTop, s.fRight, s.fBottom); const SkIRect& o = boundsCheck.mDrawnOver.getBounds(); const SkIRect& l = boundsCheck.mLastAll; const SkIRect& u = boundsCheck.mUnion; DBG_NAV_LOGD("hidden mDrawnOver={%d,%d,%d,%d} mLastAll={%d,%d,%d,%d}" " mUnion={%d,%d,%d,%d}", o.fLeft, o.fTop, o.fRight, o.fBottom, l.fLeft, l.fTop, l.fRight, l.fBottom, u.fLeft, u.fTop, u.fRight, u.fBottom); const SkIRect& a = boundsCheck.mAllDrawnIn; const WebCore::IntRect& c = mScrolledBounds; const WebCore::IntRect& b = nodeBounds; DBG_NAV_LOGD("hidden mAllDrawnIn={%d,%d,%d,%d}" " mScrolledBounds={%d,%d,%d,%d} nodeBounds={%d,%d,%d,%d}", a.fLeft, a.fTop, a.fRight, a.fBottom, c.x(), c.y(), c.right(), c.bottom(), b.x(), b.y(), b.right(), b.bottom()); DBG_NAV_LOGD("bits.mWidth=%d bits.mHeight=%d transX=%d transY=%d", marginBounds.width(),marginBounds.height(), kMargin - bounds.x(), kMargin - bounds.y()); #endif node->setDisabled(true); node->setClippedOut(unclipped == false); return true; } // was it partially occluded by later drawing? // if partially occluded, modify the bounds so that the mouse click has a better x,y const SkIRect& over = boundsCheck.mDrawnOver.getBounds(); if (over.isEmpty() == false) { #if DEBUG_NAV_UI SkIRect orig = boundsCheck.mBounds; #endif SkIRect& base = boundsCheck.mBounds; if (base.fLeft < over.fRight && base.fRight > over.fRight) base.fLeft = over.fRight; else if (base.fRight > over.fLeft && base.fLeft < over.fLeft) base.fRight = over.fLeft; if (base.fTop < over.fBottom && base.fBottom > over.fBottom) base.fTop = over.fBottom; else if (base.fBottom > over.fTop && base.fTop < over.fTop) base.fBottom = over.fTop; #if DEBUG_NAV_UI const SkIRect& modded = boundsCheck.mBounds; DBG_NAV_LOGD("partially occluded node:%p (%d) old:{%d,%d,%d,%d}" " new:{%d,%d,%d,%d}", best->mNode, best->mNode->index(), orig.fLeft, orig.fTop, orig.fRight, orig.fBottom, base.fLeft, base.fTop, base.fRight, base.fBottom); #endif best->mMouseBounds = WebCore::IntRect(bounds.x() + base.fLeft - kMargin, bounds.y() + base.fTop - kMargin, base.width(), base.height()); } return false; } const CachedNode* CachedRoot::moveCursor(Direction direction, const CachedFrame** framePtr, WebCore::IntPoint* scroll) { #ifndef NDEBUG ASSERT(CachedFrame::mDebug.mInUse); #endif CachedRoot* frame = this; const CachedNode* node = frame->document(); if (node == NULL) return NULL; if (mViewBounds.isEmpty()) return NULL; resetClippedOut(); setData(); BestData bestData; innerMove(node, &bestData, direction, scroll, true); *framePtr = bestData.mFrame; return const_cast(bestData.mNode); } void CachedRoot::reset() { #ifndef NDEBUG ASSERT(CachedFrame::mDebug.mInUse); #endif mContents = mViewBounds = WebCore::IntRect(0, 0, 0, 0); mMaxXScroll = mMaxYScroll = 0; mSelectionStart = mSelectionEnd = -1; mScrollOnly = false; } bool CachedRoot::scrollDelta(WebCore::IntRect& newOutset, Direction direction, int* delta) { switch (direction) { case LEFT: *delta = -mMaxXScroll; return newOutset.x() >= mViewBounds.x(); case RIGHT: *delta = mMaxXScroll; return newOutset.right() <= mViewBounds.right(); case UP: *delta = -mMaxYScroll; return newOutset.y() >= mViewBounds.y(); case DOWN: *delta = mMaxYScroll; return newOutset.bottom() <= mViewBounds.bottom(); default: *delta = 0; ASSERT(0); } return false; } void CachedRoot::setCachedFocus(CachedFrame* frame, CachedNode* node) { mFocusBounds = WebCore::IntRect(0, 0, 0, 0); if (node == NULL) return; node->setIsFocus(true); mFocusBounds = node->bounds(); frame->setFocusIndex(node - frame->document()); CachedFrame* parent; while ((parent = frame->parent()) != NULL) { parent->setFocusIndex(frame->indexInParent()); frame = parent; } #if DEBUG_NAV_UI const CachedNode* focus = frame->currentFocus(); WebCore::IntRect bounds = WebCore::IntRect(0, 0, 0, 0); if (focus) bounds = focus->bounds(); DBG_NAV_LOGD("new focus %d (nodePointer=%p) bounds={%d,%d,%d,%d}", focus ? focus->index() : 0, focus ? focus->nodePointer() : NULL, bounds.x(), bounds.y(), bounds.width(), bounds.height()); #endif } void CachedRoot::setCursor(CachedFrame* frame, CachedNode* node) { #if DEBUG_NAV_UI const CachedNode* cursor = currentCursor(); WebCore::IntRect bounds; if (cursor) bounds = cursor->bounds(); DBG_NAV_LOGD("old cursor %d (nodePointer=%p) bounds={%d,%d,%d,%d}", cursor ? cursor->index() : 0, cursor ? cursor->nodePointer() : NULL, bounds.x(), bounds.y(), bounds.width(), bounds.height()); #endif clearCursor(); if (node == NULL) return; node->setIsCursor(true); node->show(); frame->setCursorIndex(node - frame->document()); CachedFrame* parent; while ((parent = frame->parent()) != NULL) { parent->setCursorIndex(frame->indexInParent()); frame = parent; } #if DEBUG_NAV_UI cursor = currentCursor(); bounds = WebCore::IntRect(0, 0, 0, 0); if (cursor) bounds = cursor->bounds(); DBG_NAV_LOGD("new cursor %d (nodePointer=%p) bounds={%d,%d,%d,%d}", cursor ? cursor->index() : 0, cursor ? cursor->nodePointer() : NULL, bounds.x(), bounds.y(), bounds.width(), bounds.height()); #endif } #if DUMP_NAV_CACHE #define DEBUG_PRINT_BOOL(field) \ DUMP_NAV_LOGD("// bool " #field "=%s;\n", b->field ? "true" : "false") #define DEBUG_PRINT_RECT(field) \ { const WebCore::IntRect& r = b->field; \ DUMP_NAV_LOGD("// IntRect " #field "={%d, %d, %d, %d};\n", \ r.x(), r.y(), r.width(), r.height()); } CachedRoot* CachedRoot::Debug::base() const { CachedRoot* nav = (CachedRoot*) ((char*) this - OFFSETOF(CachedRoot, mDebug)); return nav; } void CachedRoot::Debug::print() const { #ifdef DUMP_NAV_CACHE_USING_PRINTF gWriteLogMutex.lock(); ASSERT(gNavCacheLogFile == NULL); gNavCacheLogFile = fopen(NAV_CACHE_LOG_FILE, "a"); #endif CachedRoot* b = base(); b->CachedFrame::mDebug.print(); b->mHistory->mDebug.print(b); DUMP_NAV_LOGD("// int mMaxXScroll=%d, mMaxYScroll=%d;\n", b->mMaxXScroll, b->mMaxYScroll); #ifdef DUMP_NAV_CACHE_USING_PRINTF if (gNavCacheLogFile) fclose(gNavCacheLogFile); gNavCacheLogFile = NULL; gWriteLogMutex.unlock(); #endif } #endif }