2010-07-23 fsamuel@chromium.org <fsamuel@chromium.org>
[WebKit-https.git] / WebCore / accessibility / AccessibilityTable.cpp
1 /*
2  * Copyright (C) 2008 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer.
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution.
13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission.
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28
29 #include "config.h"
30 #include "AccessibilityTable.h"
31
32 #include "AXObjectCache.h"
33 #include "AccessibilityTableCell.h"
34 #include "AccessibilityTableColumn.h"
35 #include "AccessibilityTableHeaderContainer.h"
36 #include "AccessibilityTableRow.h"
37 #include "HTMLNames.h"
38 #include "HTMLTableCaptionElement.h"
39 #include "HTMLTableCellElement.h"
40 #include "HTMLTableElement.h"
41 #include "RenderObject.h"
42 #include "RenderTable.h"
43 #include "RenderTableCell.h"
44 #include "RenderTableSection.h"
45
46 using namespace std;
47
48 namespace WebCore {
49
50 using namespace HTMLNames;
51
52 AccessibilityTable::AccessibilityTable(RenderObject* renderer)
53     : AccessibilityRenderObject(renderer),
54     m_headerContainer(0)
55 {
56 #if ACCESSIBILITY_TABLES
57     m_isAccessibilityTable = isTableExposableThroughAccessibility();
58 #else
59     m_isAccessibilityTable = false;
60 #endif
61 }
62
63 AccessibilityTable::~AccessibilityTable()
64 {
65 }
66
67 PassRefPtr<AccessibilityTable> AccessibilityTable::create(RenderObject* renderer)
68 {
69     return adoptRef(new AccessibilityTable(renderer));
70 }
71
72 bool AccessibilityTable::isTableExposableThroughAccessibility()
73 {
74     // the following is a heuristic used to determine if a
75     // <table> should be exposed as an AXTable. The goal
76     // is to only show "data" tables
77     
78     if (!m_renderer || !m_renderer->isTable())
79         return false;
80     
81     // if the developer assigned an aria role to this, then we shouldn't 
82     // expose it as a table, unless, of course, the aria role is a table
83     AccessibilityRole ariaRole = ariaRoleAttribute();
84     if (ariaRole != UnknownRole)
85         return false;
86     
87     RenderTable* table = toRenderTable(m_renderer);
88     
89     // this employs a heuristic to determine if this table should appear. 
90     // Only "data" tables should be exposed as tables. 
91     // Unfortunately, there is no good way to determine the difference
92     // between a "layout" table and a "data" table
93     
94     Node* tableNode = table->node();
95     if (!tableNode || !tableNode->hasTagName(tableTag))
96         return false;
97
98     // Gtk+ ATs expect all tables to be exposed as tables.
99 #if PLATFORM(GTK)
100     return true;
101 #endif
102     
103     // if there is a caption element, summary, THEAD, or TFOOT section, it's most certainly a data table
104     HTMLTableElement* tableElement = static_cast<HTMLTableElement*>(tableNode);
105     if (!tableElement->summary().isEmpty() || tableElement->tHead() || tableElement->tFoot() || tableElement->caption())
106         return true;
107     
108     // if someone used "rules" attribute than the table should appear
109     if (!tableElement->rules().isEmpty())
110         return true;    
111     
112     // go through the cell's and check for tell-tale signs of "data" table status
113     // cells have borders, or use attributes like headers, abbr, scope or axis
114     RenderTableSection* firstBody = table->firstBody();
115     if (!firstBody)
116         return false;
117     
118     int numCols = firstBody->numColumns();
119     int numRows = firstBody->numRows();
120     
121     // if there's only one cell, it's not a good AXTable candidate
122     if (numRows == 1 && numCols == 1)
123         return false;
124     
125     // store the background color of the table to check against cell's background colors
126     RenderStyle* tableStyle = table->style();
127     if (!tableStyle)
128         return false;
129     Color tableBGColor = tableStyle->visitedDependentColor(CSSPropertyBackgroundColor);
130     
131     // check enough of the cells to find if the table matches our criteria
132     // Criteria: 
133     //   1) must have at least one valid cell (and)
134     //   2) at least half of cells have borders (or)
135     //   3) at least half of cells have different bg colors than the table, and there is cell spacing
136     unsigned validCellCount = 0;
137     unsigned borderedCellCount = 0;
138     unsigned backgroundDifferenceCellCount = 0;
139     
140     Color alternatingRowColors[5];
141     int alternatingRowColorCount = 0;
142     
143     int headersInFirstColumnCount = 0;
144     for (int row = 0; row < numRows; ++row) {
145     
146         int headersInFirstRowCount = 0;
147         for (int col = 0; col < numCols; ++col) {    
148             RenderTableCell* cell = firstBody->primaryCellAt(row, col);
149             if (!cell)
150                 continue;
151             Node* cellNode = cell->node();
152             if (!cellNode)
153                 continue;
154             
155             if (cell->width() < 1 || cell->height() < 1)
156                 continue;
157             
158             validCellCount++;
159             
160             HTMLTableCellElement* cellElement = static_cast<HTMLTableCellElement*>(cellNode);
161             
162             bool isTHCell = cellElement->hasTagName(thTag);
163             // If the first row is comprised of all <th> tags, assume it is a data table.
164             if (!row && isTHCell)
165                 headersInFirstRowCount++;
166
167             // If the first column is comprised of all <tg> tags, assume it is a data table.
168             if (!col && isTHCell)
169                 headersInFirstColumnCount++;
170             
171             // in this case, the developer explicitly assigned a "data" table attribute
172             if (!cellElement->headers().isEmpty() || !cellElement->abbr().isEmpty()
173                 || !cellElement->axis().isEmpty() || !cellElement->scope().isEmpty())
174                 return true;
175             
176             RenderStyle* renderStyle = cell->style();
177             if (!renderStyle)
178                 continue;
179
180             // a cell needs to have matching bordered sides, before it can be considered a bordered cell.
181             if ((cell->borderTop() > 0 && cell->borderBottom() > 0)
182                 || (cell->borderLeft() > 0 && cell->borderRight() > 0))
183                 borderedCellCount++;
184             
185             // if the cell has a different color from the table and there is cell spacing,
186             // then it is probably a data table cell (spacing and colors take the place of borders)
187             Color cellColor = renderStyle->visitedDependentColor(CSSPropertyBackgroundColor);
188             if (table->hBorderSpacing() > 0 && table->vBorderSpacing() > 0
189                 && tableBGColor != cellColor && cellColor.alpha() != 1)
190                 backgroundDifferenceCellCount++;
191             
192             // if we've found 10 "good" cells, we don't need to keep searching
193             if (borderedCellCount >= 10 || backgroundDifferenceCellCount >= 10)
194                 return true;
195             
196             // For the first 5 rows, cache the background color so we can check if this table has zebra-striped rows.
197             if (row < 5 && row == alternatingRowColorCount) {
198                 RenderObject* renderRow = cell->parent();
199                 if (!renderRow || !renderRow->isTableRow())
200                     continue;
201                 RenderStyle* rowRenderStyle = renderRow->style();
202                 if (!rowRenderStyle)
203                     continue;
204                 Color rowColor = rowRenderStyle->visitedDependentColor(CSSPropertyBackgroundColor);
205                 alternatingRowColors[alternatingRowColorCount] = rowColor;
206                 alternatingRowColorCount++;
207             }
208         }
209         
210         if (!row && headersInFirstRowCount == numCols && numCols > 1)
211             return true;
212     }
213
214     if (headersInFirstColumnCount == numRows && numRows > 1)
215         return true;
216     
217     // if there is less than two valid cells, it's not a data table
218     if (validCellCount <= 1)
219         return false;
220     
221     // half of the cells had borders, it's a data table
222     unsigned neededCellCount = validCellCount / 2;
223     if (borderedCellCount >= neededCellCount)
224         return true;
225     
226     // half had different background colors, it's a data table
227     if (backgroundDifferenceCellCount >= neededCellCount)
228         return true;
229
230     // Check if there is an alternating row background color indicating a zebra striped style pattern.
231     if (alternatingRowColorCount > 2) {
232         Color firstColor = alternatingRowColors[0];
233         for (int k = 1; k < alternatingRowColorCount; k++) {
234             // If an odd row was the same color as the first row, its not alternating.
235             if (k % 2 == 1 && alternatingRowColors[k] == firstColor)
236                 return false;
237             // If an even row is not the same as the first row, its not alternating.
238             if (!(k % 2) && alternatingRowColors[k] != firstColor)
239                 return false;
240         }
241         return true;
242     }
243     
244     return false;
245 }
246     
247 void AccessibilityTable::clearChildren()
248 {
249     AccessibilityRenderObject::clearChildren();
250     m_rows.clear();
251     m_columns.clear();
252 }
253
254 void AccessibilityTable::addChildren()
255 {
256     if (!isDataTable()) {
257         AccessibilityRenderObject::addChildren();
258         return;
259     }
260     
261     ASSERT(!m_haveChildren); 
262     
263     m_haveChildren = true;
264     if (!m_renderer || !m_renderer->isTable())
265         return;
266     
267     RenderTable* table = toRenderTable(m_renderer);
268     AXObjectCache* axCache = m_renderer->document()->axObjectCache();
269
270     // go through all the available sections to pull out the rows
271     // and add them as children
272     RenderTableSection* tableSection = table->header();
273     if (!tableSection)
274         tableSection = table->firstBody();
275     
276     if (!tableSection)
277         return;
278     
279     RenderTableSection* initialTableSection = tableSection;
280     
281     while (tableSection) {
282         
283         HashSet<AccessibilityObject*> appendedRows;
284
285         unsigned numRows = tableSection->numRows();
286         unsigned numCols = tableSection->numColumns();
287         for (unsigned rowIndex = 0; rowIndex < numRows; ++rowIndex) {
288             for (unsigned colIndex = 0; colIndex < numCols; ++colIndex) {
289                 
290                 RenderTableCell* cell = tableSection->primaryCellAt(rowIndex, colIndex);
291                 if (!cell)
292                     continue;
293                 
294                 AccessibilityObject* rowObject = axCache->getOrCreate(cell->parent());
295                 if (!rowObject->isTableRow())
296                     continue;
297                 
298                 AccessibilityTableRow* row = static_cast<AccessibilityTableRow*>(rowObject);
299                 // we need to check every cell for a new row, because cell spans
300                 // can cause us to mess rows if we just check the first column
301                 if (appendedRows.contains(row))
302                     continue;
303                 
304                 row->setRowIndex((int)m_rows.size());        
305                 m_rows.append(row);
306                 if (!row->accessibilityIsIgnored())
307                     m_children.append(row);
308 #if PLATFORM(GTK)
309                 else
310                     m_children.append(row->children());
311 #endif
312                 appendedRows.add(row);
313             }
314         }
315         
316         tableSection = table->sectionBelow(tableSection, true);
317     }
318     
319     // make the columns based on the number of columns in the first body
320     unsigned length = initialTableSection->numColumns();
321     for (unsigned i = 0; i < length; ++i) {
322         AccessibilityTableColumn* column = static_cast<AccessibilityTableColumn*>(axCache->getOrCreate(ColumnRole));
323         column->setColumnIndex((int)i);
324         column->setParentTable(this);
325         m_columns.append(column);
326         if (!column->accessibilityIsIgnored())
327             m_children.append(column);
328     }
329     
330     AccessibilityObject* headerContainerObject = headerContainer();
331     if (headerContainerObject && !headerContainerObject->accessibilityIsIgnored())
332         m_children.append(headerContainerObject);
333 }
334     
335 AccessibilityObject* AccessibilityTable::headerContainer()
336 {
337     if (m_headerContainer)
338         return m_headerContainer;
339     
340     m_headerContainer = static_cast<AccessibilityTableHeaderContainer*>(axObjectCache()->getOrCreate(TableHeaderContainerRole));
341     m_headerContainer->setParentTable(this);
342     
343     return m_headerContainer;
344 }
345
346 AccessibilityObject::AccessibilityChildrenVector& AccessibilityTable::columns()
347 {
348     updateChildrenIfNecessary();
349         
350     return m_columns;
351 }
352
353 AccessibilityObject::AccessibilityChildrenVector& AccessibilityTable::rows()
354 {
355     updateChildrenIfNecessary();
356     
357     return m_rows;
358 }
359     
360 void AccessibilityTable::rowHeaders(AccessibilityChildrenVector& headers)
361 {
362     if (!m_renderer)
363         return;
364     
365     updateChildrenIfNecessary();
366     
367     unsigned rowCount = m_rows.size();
368     for (unsigned k = 0; k < rowCount; ++k) {
369         AccessibilityObject* header = static_cast<AccessibilityTableRow*>(m_rows[k].get())->headerObject();
370         if (!header)
371             continue;
372         headers.append(header);
373     }
374 }
375
376 void AccessibilityTable::columnHeaders(AccessibilityChildrenVector& headers)
377 {
378     if (!m_renderer)
379         return;
380     
381     updateChildrenIfNecessary();
382     
383     unsigned colCount = m_columns.size();
384     for (unsigned k = 0; k < colCount; ++k) {
385         AccessibilityObject* header = static_cast<AccessibilityTableColumn*>(m_columns[k].get())->headerObject();
386         if (!header)
387             continue;
388         headers.append(header);
389     }
390 }
391     
392 void AccessibilityTable::cells(AccessibilityObject::AccessibilityChildrenVector& cells)
393 {
394     if (!m_renderer)
395         return;
396     
397     updateChildrenIfNecessary();
398     
399     int numRows = m_rows.size();
400     for (int row = 0; row < numRows; ++row) {
401         AccessibilityChildrenVector rowChildren = m_rows[row]->children();
402         cells.append(rowChildren);
403     }
404 }
405     
406 unsigned AccessibilityTable::columnCount()
407 {
408     updateChildrenIfNecessary();
409     
410     return m_columns.size();    
411 }
412     
413 unsigned AccessibilityTable::rowCount()
414 {
415     updateChildrenIfNecessary();
416     
417     return m_rows.size();
418 }
419     
420 AccessibilityTableCell* AccessibilityTable::cellForColumnAndRow(unsigned column, unsigned row)
421 {
422     if (!m_renderer || !m_renderer->isTable())
423         return 0;
424     
425     updateChildrenIfNecessary();
426     
427     RenderTable* table = toRenderTable(m_renderer);
428     RenderTableSection* tableSection = table->header();
429     if (!tableSection)
430         tableSection = table->firstBody();
431     
432     RenderTableCell* cell = 0;
433     unsigned rowCount = 0;
434     unsigned rowOffset = 0;
435     while (tableSection) {
436         
437         unsigned numRows = tableSection->numRows();
438         unsigned numCols = tableSection->numColumns();
439         
440         rowCount += numRows;
441         
442         unsigned sectionSpecificRow = row - rowOffset;            
443         if (row < rowCount && column < numCols && sectionSpecificRow < numRows) {
444             cell = tableSection->primaryCellAt(sectionSpecificRow, column);
445             
446             // we didn't find the cell, which means there's spanning happening
447             // search backwards to find the spanning cell
448             if (!cell) {
449                 
450                 // first try rows
451                 for (int testRow = sectionSpecificRow-1; testRow >= 0; --testRow) {
452                     cell = tableSection->primaryCellAt(testRow, column);
453                     // cell overlapped. use this one
454                     if (cell && ((cell->row() + (cell->rowSpan()-1)) >= (int)sectionSpecificRow))
455                         break;
456                     cell = 0;
457                 }
458                 
459                 if (!cell) {
460                     // try cols
461                     for (int testCol = column-1; testCol >= 0; --testCol) {
462                         cell = tableSection->primaryCellAt(sectionSpecificRow, testCol);
463                         // cell overlapped. use this one
464                         if (cell && ((cell->col() + (cell->colSpan()-1)) >= (int)column))
465                             break;
466                         cell = 0;
467                     }
468                 }
469             }
470         }
471         
472         if (cell)
473             break;
474         
475         rowOffset += numRows;
476         // we didn't find anything between the rows we should have
477         if (row < rowCount)
478             break;
479         tableSection = table->sectionBelow(tableSection, true);        
480     }
481     
482     if (!cell)
483         return 0;
484     
485     AccessibilityObject* cellObject = axObjectCache()->getOrCreate(cell);
486     ASSERT(cellObject->isTableCell());
487     
488     return static_cast<AccessibilityTableCell*>(cellObject);
489 }
490
491 AccessibilityRole AccessibilityTable::roleValue() const
492 {
493     if (!isDataTable())
494         return AccessibilityRenderObject::roleValue();
495
496     return TableRole;
497 }
498     
499 bool AccessibilityTable::accessibilityIsIgnored() const
500 {
501     AccessibilityObjectInclusion decision = accessibilityIsIgnoredBase();
502     if (decision == IncludeObject)
503         return false;
504     if (decision == IgnoreObject)
505         return true;
506     
507     if (!isDataTable())
508         return AccessibilityRenderObject::accessibilityIsIgnored();
509         
510     return false;
511 }
512     
513 String AccessibilityTable::title() const
514 {
515     if (!isDataTable())
516         return AccessibilityRenderObject::title();
517     
518     String title;
519     if (!m_renderer)
520         return title;
521     
522     // see if there is a caption
523     Node* tableElement = m_renderer->node();
524     if (tableElement && tableElement->hasTagName(tableTag)) {
525         HTMLTableCaptionElement* caption = static_cast<HTMLTableElement*>(tableElement)->caption();
526         if (caption)
527             title = caption->innerText();
528     }
529     
530     // try the standard 
531     if (title.isEmpty())
532         title = AccessibilityRenderObject::title();
533     
534     return title;
535 }
536
537 bool AccessibilityTable::isDataTable() const
538 {
539     if (!m_renderer)
540         return false;
541     
542     return m_isAccessibilityTable;
543 }
544
545 } // namespace WebCore