1/*
2 * Copyright (C) 2004, 2005, 2006, 2007, 2008 Apple Inc. All rights reserved.
3 * Copyright (C) 2006 Alexey Proskuryakov (ap@nypop.com)
4 * Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
9 * 1. Redistributions of source code must retain the above copyright
10 *    notice, this list of conditions and the following disclaimer.
11 * 2. Redistributions in binary form must reproduce the above copyright
12 *    notice, this list of conditions and the following disclaimer in the
13 *    documentation and/or other materials provided with the distribution.
14 *
15 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
16 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
19 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
23 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 */
27
28#import "config.h"
29#import "Frame.h"
30
31#import "BlockExceptions.h"
32#import "ColorMac.h"
33#import "Cursor.h"
34#import "DOMInternal.h"
35#import "Event.h"
36#import "FrameLoaderClient.h"
37#import "FrameView.h"
38#import "GraphicsContext.h"
39#import "HTMLNames.h"
40#import "HTMLTableCellElement.h"
41#import "HitTestRequest.h"
42#import "HitTestResult.h"
43#import "KeyboardEvent.h"
44#import "Logging.h"
45#import "MouseEventWithHitTestResults.h"
46#import "Page.h"
47#import "PlatformKeyboardEvent.h"
48#import "PlatformWheelEvent.h"
49#import "RegularExpression.h"
50#import "RenderTableCell.h"
51#import "Scrollbar.h"
52#import "SimpleFontData.h"
53#import "WebCoreViewFactory.h"
54#import "visible_units.h"
55#import <Carbon/Carbon.h>
56#import <wtf/StdLibExtras.h>
57
58@interface NSView (WebCoreHTMLDocumentView)
59- (void)drawSingleRect:(NSRect)rect;
60@end
61
62using namespace std;
63
64namespace WebCore {
65
66using namespace HTMLNames;
67
68// Either get cached regexp or build one that matches any of the labels.
69// The regexp we build is of the form:  (STR1|STR2|STRN)
70static RegularExpression* regExpForLabels(NSArray* labels)
71{
72    // All the ObjC calls in this method are simple array and string
73    // calls which we can assume do not raise exceptions
74
75
76    // Parallel arrays that we use to cache regExps.  In practice the number of expressions
77    // that the app will use is equal to the number of locales is used in searching.
78    static const unsigned int regExpCacheSize = 4;
79    static NSMutableArray* regExpLabels = nil;
80    DEFINE_STATIC_LOCAL(Vector<RegularExpression*>, regExps, ());
81    DEFINE_STATIC_LOCAL(RegularExpression, wordRegExp, ("\\w", TextCaseSensitive));
82
83    RegularExpression* result;
84    if (!regExpLabels)
85        regExpLabels = [[NSMutableArray alloc] initWithCapacity:regExpCacheSize];
86    CFIndex cacheHit = [regExpLabels indexOfObject:labels];
87    if (cacheHit != NSNotFound)
88        result = regExps.at(cacheHit);
89    else {
90        String pattern("(");
91        unsigned int numLabels = [labels count];
92        unsigned int i;
93        for (i = 0; i < numLabels; i++) {
94            String label = [labels objectAtIndex:i];
95
96            bool startsWithWordChar = false;
97            bool endsWithWordChar = false;
98            if (label.length() != 0) {
99                startsWithWordChar = wordRegExp.match(label.substring(0, 1)) >= 0;
100                endsWithWordChar = wordRegExp.match(label.substring(label.length() - 1, 1)) >= 0;
101            }
102
103            if (i != 0)
104                pattern.append("|");
105            // Search for word boundaries only if label starts/ends with "word characters".
106            // If we always searched for word boundaries, this wouldn't work for languages
107            // such as Japanese.
108            if (startsWithWordChar)
109                pattern.append("\\b");
110            pattern.append(label);
111            if (endsWithWordChar)
112                pattern.append("\\b");
113        }
114        pattern.append(")");
115        result = new RegularExpression(pattern, TextCaseInsensitive);
116    }
117
118    // add regexp to the cache, making sure it is at the front for LRU ordering
119    if (cacheHit != 0) {
120        if (cacheHit != NSNotFound) {
121            // remove from old spot
122            [regExpLabels removeObjectAtIndex:cacheHit];
123            regExps.remove(cacheHit);
124        }
125        // add to start
126        [regExpLabels insertObject:labels atIndex:0];
127        regExps.insert(0, result);
128        // trim if too big
129        if ([regExpLabels count] > regExpCacheSize) {
130            [regExpLabels removeObjectAtIndex:regExpCacheSize];
131            RegularExpression* last = regExps.last();
132            regExps.removeLast();
133            delete last;
134        }
135    }
136    return result;
137}
138
139NSString* Frame::searchForLabelsBeforeElement(NSArray* labels, Element* element, size_t* resultDistance, bool* resultIsInCellAbove)
140{
141    RegularExpression* regExp = regExpForLabels(labels);
142    // We stop searching after we've seen this many chars
143    const unsigned int charsSearchedThreshold = 500;
144    // This is the absolute max we search.  We allow a little more slop than
145    // charsSearchedThreshold, to make it more likely that we'll search whole nodes.
146    const unsigned int maxCharsSearched = 600;
147    // If the starting element is within a table, the cell that contains it
148    HTMLTableCellElement* startingTableCell = 0;
149    bool searchedCellAbove = false;
150
151    if (resultDistance)
152        *resultDistance = notFound;
153    if (resultIsInCellAbove)
154        *resultIsInCellAbove = false;
155
156    // walk backwards in the node tree, until another element, or form, or end of tree
157    unsigned lengthSearched = 0;
158    Node* n;
159    for (n = element->traversePreviousNode();
160         n && lengthSearched < charsSearchedThreshold;
161         n = n->traversePreviousNode())
162    {
163        if (n->hasTagName(formTag)
164            || (n->isHTMLElement() && static_cast<Element*>(n)->isFormControlElement()))
165        {
166            // We hit another form element or the start of the form - bail out
167            break;
168        } else if (n->hasTagName(tdTag) && !startingTableCell) {
169            startingTableCell = static_cast<HTMLTableCellElement*>(n);
170        } else if (n->hasTagName(trTag) && startingTableCell) {
171            NSString* result = searchForLabelsAboveCell(regExp, startingTableCell, resultDistance);
172            if (result && [result length] > 0) {
173                if (resultIsInCellAbove)
174                    *resultIsInCellAbove = true;
175                return result;
176            }
177            searchedCellAbove = true;
178        } else if (n->isTextNode() && n->renderer() && n->renderer()->style()->visibility() == VISIBLE) {
179            // For each text chunk, run the regexp
180            String nodeString = n->nodeValue();
181            // add 100 for slop, to make it more likely that we'll search whole nodes
182            if (lengthSearched + nodeString.length() > maxCharsSearched)
183                nodeString = nodeString.right(charsSearchedThreshold - lengthSearched);
184            int pos = regExp->searchRev(nodeString);
185            if (pos >= 0) {
186                if (resultDistance)
187                    *resultDistance = lengthSearched;
188                return nodeString.substring(pos, regExp->matchedLength());
189            }
190            lengthSearched += nodeString.length();
191        }
192    }
193
194    // If we started in a cell, but bailed because we found the start of the form or the
195    // previous element, we still might need to search the row above us for a label.
196    if (startingTableCell && !searchedCellAbove) {
197        NSString* result = searchForLabelsAboveCell(regExp, startingTableCell, resultDistance);
198        if (result && [result length] > 0) {
199            if (resultIsInCellAbove)
200                *resultIsInCellAbove = true;
201            return result;
202        }
203    }
204
205    return nil;
206}
207
208static NSString *matchLabelsAgainstString(NSArray *labels, const String& stringToMatch)
209{
210    if (stringToMatch.isEmpty())
211        return nil;
212
213    String mutableStringToMatch = stringToMatch;
214
215    // Make numbers and _'s in field names behave like word boundaries, e.g., "address2"
216    replace(mutableStringToMatch, RegularExpression("\\d", TextCaseSensitive), " ");
217    mutableStringToMatch.replace('_', ' ');
218
219    RegularExpression* regExp = regExpForLabels(labels);
220    // Use the largest match we can find in the whole string
221    int pos;
222    int length;
223    int bestPos = -1;
224    int bestLength = -1;
225    int start = 0;
226    do {
227        pos = regExp->match(mutableStringToMatch, start);
228        if (pos != -1) {
229            length = regExp->matchedLength();
230            if (length >= bestLength) {
231                bestPos = pos;
232                bestLength = length;
233            }
234            start = pos + 1;
235        }
236    } while (pos != -1);
237
238    if (bestPos != -1)
239        return mutableStringToMatch.substring(bestPos, bestLength);
240    return nil;
241}
242
243NSString* Frame::matchLabelsAgainstElement(NSArray* labels, Element* element)
244{
245    // Match against the name element, then against the id element if no match is found for the name element.
246    // See 7538330 for one popular site that benefits from the id element check.
247    // FIXME: This code is mirrored in Frame.cpp. It would be nice to make the Mac code call the platform-agnostic
248    // code, which would require converting the NSArray of NSStrings to a Vector of Strings somewhere along the way.
249    String resultFromNameAttribute = matchLabelsAgainstString(labels, element->getAttribute(nameAttr));
250    if (!resultFromNameAttribute.isEmpty())
251        return resultFromNameAttribute;
252
253    return matchLabelsAgainstString(labels, element->getAttribute(idAttr));
254}
255
256NSImage* Frame::imageFromRect(NSRect rect) const
257{
258    PaintBehavior oldBehavior = m_view->paintBehavior();
259    m_view->setPaintBehavior(oldBehavior | PaintBehaviorFlattenCompositingLayers);
260
261    BEGIN_BLOCK_OBJC_EXCEPTIONS;
262
263    NSImage* resultImage = [[[NSImage alloc] initWithSize:rect.size] autorelease];
264
265    if (rect.size.width != 0 && rect.size.height != 0) {
266        [resultImage setFlipped:YES];
267        [resultImage lockFocus];
268
269        GraphicsContext graphicsContext((CGContextRef)[[NSGraphicsContext currentContext] graphicsPort]);
270        graphicsContext.save();
271        graphicsContext.translate(-rect.origin.x, -rect.origin.y);
272        m_view->paintContents(&graphicsContext, IntRect(rect));
273        graphicsContext.restore();
274
275        [resultImage unlockFocus];
276        [resultImage setFlipped:NO];
277    }
278
279    m_view->setPaintBehavior(oldBehavior);
280    return resultImage;
281
282    END_BLOCK_OBJC_EXCEPTIONS;
283
284    m_view->setPaintBehavior(oldBehavior);
285    return nil;
286}
287
288NSImage* Frame::selectionImage(bool forceBlackText) const
289{
290    m_view->setPaintBehavior(PaintBehaviorSelectionOnly | (forceBlackText ? PaintBehaviorForceBlackText : 0));
291    m_doc->updateLayout();
292    NSImage* result = imageFromRect(selection()->bounds());
293    m_view->setPaintBehavior(PaintBehaviorNormal);
294    return result;
295}
296
297NSImage* Frame::snapshotDragImage(Node* node, NSRect* imageRect, NSRect* elementRect) const
298{
299    RenderObject* renderer = node->renderer();
300    if (!renderer)
301        return nil;
302
303    renderer->updateDragState(true);    // mark dragged nodes (so they pick up the right CSS)
304    m_doc->updateLayout();        // forces style recalc - needed since changing the drag state might
305                                        // imply new styles, plus JS could have changed other things
306    IntRect topLevelRect;
307    NSRect paintingRect = renderer->paintingRootRect(topLevelRect);
308
309    m_view->setNodeToDraw(node);              // invoke special sub-tree drawing mode
310    NSImage* result = imageFromRect(paintingRect);
311    renderer->updateDragState(false);
312    m_doc->updateLayout();
313    m_view->setNodeToDraw(0);
314
315    if (elementRect)
316        *elementRect = topLevelRect;
317    if (imageRect)
318        *imageRect = paintingRect;
319    return result;
320}
321
322DragImageRef Frame::nodeImage(Node* node)
323{
324    RenderObject* renderer = node->renderer();
325    if (!renderer)
326        return nil;
327
328    m_doc->updateLayout(); // forces style recalc
329
330    IntRect topLevelRect;
331    NSRect paintingRect = renderer->paintingRootRect(topLevelRect);
332
333    m_view->setNodeToDraw(node); // invoke special sub-tree drawing mode
334    NSImage* result = imageFromRect(paintingRect);
335    m_view->setNodeToDraw(0);
336
337    return result;
338}
339
340DragImageRef Frame::dragImageForSelection()
341{
342    if (!selection()->isRange())
343        return nil;
344    return selectionImage();
345}
346
347} // namespace WebCore
348