1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 
6 #include "ARIAGridAccessible-inl.h"
7 
8 #include "LocalAccessible-inl.h"
9 #include "AccAttributes.h"
10 #include "AccIterator.h"
11 #include "nsAccUtils.h"
12 #include "Role.h"
13 #include "States.h"
14 
15 #include "mozilla/dom/Element.h"
16 #include "nsComponentManagerUtils.h"
17 
18 using namespace mozilla;
19 using namespace mozilla::a11y;
20 
21 ////////////////////////////////////////////////////////////////////////////////
22 // ARIAGridAccessible
23 ////////////////////////////////////////////////////////////////////////////////
24 
25 ////////////////////////////////////////////////////////////////////////////////
26 // Constructor
27 
ARIAGridAccessible(nsIContent * aContent,DocAccessible * aDoc)28 ARIAGridAccessible::ARIAGridAccessible(nsIContent* aContent,
29                                        DocAccessible* aDoc)
30     : HyperTextAccessibleWrap(aContent, aDoc) {
31   mGenericTypes |= eTable;
32 }
33 
NativeRole() const34 role ARIAGridAccessible::NativeRole() const {
35   a11y::role r = GetAccService()->MarkupRole(mContent);
36   return r != roles::NOTHING ? r : roles::TABLE;
37 }
38 
NativeAttributes()39 already_AddRefed<AccAttributes> ARIAGridAccessible::NativeAttributes() {
40   RefPtr<AccAttributes> attributes = AccessibleWrap::NativeAttributes();
41 
42   if (IsProbablyLayoutTable()) {
43     attributes->SetAttribute(nsGkAtoms::layout_guess, true);
44   }
45 
46   return attributes.forget();
47 }
48 
49 ////////////////////////////////////////////////////////////////////////////////
50 // Table
51 
ColCount() const52 uint32_t ARIAGridAccessible::ColCount() const {
53   AccIterator rowIter(this, filters::GetRow);
54   LocalAccessible* row = rowIter.Next();
55   if (!row) return 0;
56 
57   AccIterator cellIter(row, filters::GetCell);
58   LocalAccessible* cell = nullptr;
59 
60   uint32_t colCount = 0;
61   while ((cell = cellIter.Next())) {
62     MOZ_ASSERT(cell->IsTableCell(), "No table or grid cell!");
63     colCount += cell->AsTableCell()->ColExtent();
64   }
65 
66   return colCount;
67 }
68 
RowCount()69 uint32_t ARIAGridAccessible::RowCount() {
70   uint32_t rowCount = 0;
71   AccIterator rowIter(this, filters::GetRow);
72   while (rowIter.Next()) rowCount++;
73 
74   return rowCount;
75 }
76 
CellAt(uint32_t aRowIndex,uint32_t aColumnIndex)77 LocalAccessible* ARIAGridAccessible::CellAt(uint32_t aRowIndex,
78                                             uint32_t aColumnIndex) {
79   LocalAccessible* row = RowAt(aRowIndex);
80   if (!row) return nullptr;
81 
82   return CellInRowAt(row, aColumnIndex);
83 }
84 
IsColSelected(uint32_t aColIdx)85 bool ARIAGridAccessible::IsColSelected(uint32_t aColIdx) {
86   if (IsARIARole(nsGkAtoms::table)) return false;
87 
88   AccIterator rowIter(this, filters::GetRow);
89   LocalAccessible* row = rowIter.Next();
90   if (!row) return false;
91 
92   do {
93     if (!nsAccUtils::IsARIASelected(row)) {
94       LocalAccessible* cell = CellInRowAt(row, aColIdx);
95       if (!cell || !nsAccUtils::IsARIASelected(cell)) return false;
96     }
97   } while ((row = rowIter.Next()));
98 
99   return true;
100 }
101 
IsRowSelected(uint32_t aRowIdx)102 bool ARIAGridAccessible::IsRowSelected(uint32_t aRowIdx) {
103   if (IsARIARole(nsGkAtoms::table)) return false;
104 
105   LocalAccessible* row = RowAt(aRowIdx);
106   if (!row) return false;
107 
108   if (!nsAccUtils::IsARIASelected(row)) {
109     AccIterator cellIter(row, filters::GetCell);
110     LocalAccessible* cell = nullptr;
111     while ((cell = cellIter.Next())) {
112       if (!nsAccUtils::IsARIASelected(cell)) return false;
113     }
114   }
115 
116   return true;
117 }
118 
IsCellSelected(uint32_t aRowIdx,uint32_t aColIdx)119 bool ARIAGridAccessible::IsCellSelected(uint32_t aRowIdx, uint32_t aColIdx) {
120   if (IsARIARole(nsGkAtoms::table)) return false;
121 
122   LocalAccessible* row = RowAt(aRowIdx);
123   if (!row) return false;
124 
125   if (!nsAccUtils::IsARIASelected(row)) {
126     LocalAccessible* cell = CellInRowAt(row, aColIdx);
127     if (!cell || !nsAccUtils::IsARIASelected(cell)) return false;
128   }
129 
130   return true;
131 }
132 
SelectedCellCount()133 uint32_t ARIAGridAccessible::SelectedCellCount() {
134   if (IsARIARole(nsGkAtoms::table)) return 0;
135 
136   uint32_t count = 0, colCount = ColCount();
137 
138   AccIterator rowIter(this, filters::GetRow);
139   LocalAccessible* row = nullptr;
140 
141   while ((row = rowIter.Next())) {
142     if (nsAccUtils::IsARIASelected(row)) {
143       count += colCount;
144       continue;
145     }
146 
147     AccIterator cellIter(row, filters::GetCell);
148     LocalAccessible* cell = nullptr;
149 
150     while ((cell = cellIter.Next())) {
151       if (nsAccUtils::IsARIASelected(cell)) count++;
152     }
153   }
154 
155   return count;
156 }
157 
SelectedColCount()158 uint32_t ARIAGridAccessible::SelectedColCount() {
159   if (IsARIARole(nsGkAtoms::table)) return 0;
160 
161   uint32_t colCount = ColCount();
162   if (!colCount) return 0;
163 
164   AccIterator rowIter(this, filters::GetRow);
165   LocalAccessible* row = rowIter.Next();
166   if (!row) return 0;
167 
168   nsTArray<bool> isColSelArray(colCount);
169   isColSelArray.AppendElements(colCount);
170   memset(isColSelArray.Elements(), true, colCount * sizeof(bool));
171 
172   uint32_t selColCount = colCount;
173   do {
174     if (nsAccUtils::IsARIASelected(row)) continue;
175 
176     AccIterator cellIter(row, filters::GetCell);
177     LocalAccessible* cell = nullptr;
178     for (uint32_t colIdx = 0; (cell = cellIter.Next()) && colIdx < colCount;
179          colIdx++) {
180       if (isColSelArray[colIdx] && !nsAccUtils::IsARIASelected(cell)) {
181         isColSelArray[colIdx] = false;
182         selColCount--;
183       }
184     }
185   } while ((row = rowIter.Next()));
186 
187   return selColCount;
188 }
189 
SelectedRowCount()190 uint32_t ARIAGridAccessible::SelectedRowCount() {
191   if (IsARIARole(nsGkAtoms::table)) return 0;
192 
193   uint32_t count = 0;
194 
195   AccIterator rowIter(this, filters::GetRow);
196   LocalAccessible* row = nullptr;
197 
198   while ((row = rowIter.Next())) {
199     if (nsAccUtils::IsARIASelected(row)) {
200       count++;
201       continue;
202     }
203 
204     AccIterator cellIter(row, filters::GetCell);
205     LocalAccessible* cell = cellIter.Next();
206     if (!cell) continue;
207 
208     bool isRowSelected = true;
209     do {
210       if (!nsAccUtils::IsARIASelected(cell)) {
211         isRowSelected = false;
212         break;
213       }
214     } while ((cell = cellIter.Next()));
215 
216     if (isRowSelected) count++;
217   }
218 
219   return count;
220 }
221 
SelectedCells(nsTArray<LocalAccessible * > * aCells)222 void ARIAGridAccessible::SelectedCells(nsTArray<LocalAccessible*>* aCells) {
223   if (IsARIARole(nsGkAtoms::table)) return;
224 
225   AccIterator rowIter(this, filters::GetRow);
226 
227   LocalAccessible* row = nullptr;
228   while ((row = rowIter.Next())) {
229     AccIterator cellIter(row, filters::GetCell);
230     LocalAccessible* cell = nullptr;
231 
232     if (nsAccUtils::IsARIASelected(row)) {
233       while ((cell = cellIter.Next())) aCells->AppendElement(cell);
234 
235       continue;
236     }
237 
238     while ((cell = cellIter.Next())) {
239       if (nsAccUtils::IsARIASelected(cell)) aCells->AppendElement(cell);
240     }
241   }
242 }
243 
SelectedCellIndices(nsTArray<uint32_t> * aCells)244 void ARIAGridAccessible::SelectedCellIndices(nsTArray<uint32_t>* aCells) {
245   if (IsARIARole(nsGkAtoms::table)) return;
246 
247   uint32_t colCount = ColCount();
248 
249   AccIterator rowIter(this, filters::GetRow);
250   LocalAccessible* row = nullptr;
251   for (uint32_t rowIdx = 0; (row = rowIter.Next()); rowIdx++) {
252     if (nsAccUtils::IsARIASelected(row)) {
253       for (uint32_t colIdx = 0; colIdx < colCount; colIdx++) {
254         aCells->AppendElement(rowIdx * colCount + colIdx);
255       }
256 
257       continue;
258     }
259 
260     AccIterator cellIter(row, filters::GetCell);
261     LocalAccessible* cell = nullptr;
262     for (uint32_t colIdx = 0; (cell = cellIter.Next()); colIdx++) {
263       if (nsAccUtils::IsARIASelected(cell)) {
264         aCells->AppendElement(rowIdx * colCount + colIdx);
265       }
266     }
267   }
268 }
269 
SelectedColIndices(nsTArray<uint32_t> * aCols)270 void ARIAGridAccessible::SelectedColIndices(nsTArray<uint32_t>* aCols) {
271   if (IsARIARole(nsGkAtoms::table)) return;
272 
273   uint32_t colCount = ColCount();
274   if (!colCount) return;
275 
276   AccIterator rowIter(this, filters::GetRow);
277   LocalAccessible* row = rowIter.Next();
278   if (!row) return;
279 
280   nsTArray<bool> isColSelArray(colCount);
281   isColSelArray.AppendElements(colCount);
282   memset(isColSelArray.Elements(), true, colCount * sizeof(bool));
283 
284   do {
285     if (nsAccUtils::IsARIASelected(row)) continue;
286 
287     AccIterator cellIter(row, filters::GetCell);
288     LocalAccessible* cell = nullptr;
289     for (uint32_t colIdx = 0; (cell = cellIter.Next()) && colIdx < colCount;
290          colIdx++) {
291       if (isColSelArray[colIdx] && !nsAccUtils::IsARIASelected(cell)) {
292         isColSelArray[colIdx] = false;
293       }
294     }
295   } while ((row = rowIter.Next()));
296 
297   for (uint32_t colIdx = 0; colIdx < colCount; colIdx++) {
298     if (isColSelArray[colIdx]) aCols->AppendElement(colIdx);
299   }
300 }
301 
SelectedRowIndices(nsTArray<uint32_t> * aRows)302 void ARIAGridAccessible::SelectedRowIndices(nsTArray<uint32_t>* aRows) {
303   if (IsARIARole(nsGkAtoms::table)) return;
304 
305   AccIterator rowIter(this, filters::GetRow);
306   LocalAccessible* row = nullptr;
307   for (uint32_t rowIdx = 0; (row = rowIter.Next()); rowIdx++) {
308     if (nsAccUtils::IsARIASelected(row)) {
309       aRows->AppendElement(rowIdx);
310       continue;
311     }
312 
313     AccIterator cellIter(row, filters::GetCell);
314     LocalAccessible* cell = cellIter.Next();
315     if (!cell) continue;
316 
317     bool isRowSelected = true;
318     do {
319       if (!nsAccUtils::IsARIASelected(cell)) {
320         isRowSelected = false;
321         break;
322       }
323     } while ((cell = cellIter.Next()));
324 
325     if (isRowSelected) aRows->AppendElement(rowIdx);
326   }
327 }
328 
SelectRow(uint32_t aRowIdx)329 void ARIAGridAccessible::SelectRow(uint32_t aRowIdx) {
330   if (IsARIARole(nsGkAtoms::table)) return;
331 
332   AccIterator rowIter(this, filters::GetRow);
333 
334   LocalAccessible* row = nullptr;
335   for (uint32_t rowIdx = 0; (row = rowIter.Next()); rowIdx++) {
336     DebugOnly<nsresult> rv = SetARIASelected(row, rowIdx == aRowIdx);
337     NS_ASSERTION(NS_SUCCEEDED(rv), "SetARIASelected() Shouldn't fail!");
338   }
339 }
340 
SelectCol(uint32_t aColIdx)341 void ARIAGridAccessible::SelectCol(uint32_t aColIdx) {
342   if (IsARIARole(nsGkAtoms::table)) return;
343 
344   AccIterator rowIter(this, filters::GetRow);
345 
346   LocalAccessible* row = nullptr;
347   while ((row = rowIter.Next())) {
348     // Unselect all cells in the row.
349     DebugOnly<nsresult> rv = SetARIASelected(row, false);
350     NS_ASSERTION(NS_SUCCEEDED(rv), "SetARIASelected() Shouldn't fail!");
351 
352     // Select cell at the column index.
353     LocalAccessible* cell = CellInRowAt(row, aColIdx);
354     if (cell) SetARIASelected(cell, true);
355   }
356 }
357 
UnselectRow(uint32_t aRowIdx)358 void ARIAGridAccessible::UnselectRow(uint32_t aRowIdx) {
359   if (IsARIARole(nsGkAtoms::table)) return;
360 
361   LocalAccessible* row = RowAt(aRowIdx);
362   if (row) SetARIASelected(row, false);
363 }
364 
UnselectCol(uint32_t aColIdx)365 void ARIAGridAccessible::UnselectCol(uint32_t aColIdx) {
366   if (IsARIARole(nsGkAtoms::table)) return;
367 
368   AccIterator rowIter(this, filters::GetRow);
369 
370   LocalAccessible* row = nullptr;
371   while ((row = rowIter.Next())) {
372     LocalAccessible* cell = CellInRowAt(row, aColIdx);
373     if (cell) SetARIASelected(cell, false);
374   }
375 }
376 
377 ////////////////////////////////////////////////////////////////////////////////
378 // Protected
379 
SetARIASelected(LocalAccessible * aAccessible,bool aIsSelected,bool aNotify)380 nsresult ARIAGridAccessible::SetARIASelected(LocalAccessible* aAccessible,
381                                              bool aIsSelected, bool aNotify) {
382   if (IsARIARole(nsGkAtoms::table)) return NS_OK;
383 
384   nsIContent* content = aAccessible->GetContent();
385   NS_ENSURE_STATE(content);
386 
387   nsresult rv = NS_OK;
388   if (content->IsElement()) {
389     if (aIsSelected) {
390       rv = content->AsElement()->SetAttr(
391           kNameSpaceID_None, nsGkAtoms::aria_selected, u"true"_ns, aNotify);
392     } else {
393       rv = content->AsElement()->SetAttr(
394           kNameSpaceID_None, nsGkAtoms::aria_selected, u"false"_ns, aNotify);
395     }
396   }
397 
398   NS_ENSURE_SUCCESS(rv, rv);
399 
400   // No "smart" select/unselect for internal call.
401   if (!aNotify) return NS_OK;
402 
403   // If row or cell accessible was selected then we're able to not bother about
404   // selection of its cells or its row because our algorithm is row oriented,
405   // i.e. we check selection on row firstly and then on cells.
406   if (aIsSelected) return NS_OK;
407 
408   roles::Role role = aAccessible->Role();
409 
410   // If the given accessible is row that was unselected then remove
411   // aria-selected from cell accessible.
412   if (role == roles::ROW) {
413     AccIterator cellIter(aAccessible, filters::GetCell);
414     LocalAccessible* cell = nullptr;
415 
416     while ((cell = cellIter.Next())) {
417       rv = SetARIASelected(cell, false, false);
418       NS_ENSURE_SUCCESS(rv, rv);
419     }
420     return NS_OK;
421   }
422 
423   // If the given accessible is cell that was unselected and its row is selected
424   // then remove aria-selected from row and put aria-selected on
425   // siblings cells.
426   if (role == roles::GRID_CELL || role == roles::ROWHEADER ||
427       role == roles::COLUMNHEADER) {
428     LocalAccessible* row = aAccessible->LocalParent();
429 
430     if (row && row->Role() == roles::ROW && nsAccUtils::IsARIASelected(row)) {
431       rv = SetARIASelected(row, false, false);
432       NS_ENSURE_SUCCESS(rv, rv);
433 
434       AccIterator cellIter(row, filters::GetCell);
435       LocalAccessible* cell = nullptr;
436       while ((cell = cellIter.Next())) {
437         if (cell != aAccessible) {
438           rv = SetARIASelected(cell, true, false);
439           NS_ENSURE_SUCCESS(rv, rv);
440         }
441       }
442     }
443   }
444 
445   return NS_OK;
446 }
447 
448 ////////////////////////////////////////////////////////////////////////////////
449 // ARIARowAccessible
450 ////////////////////////////////////////////////////////////////////////////////
451 
ARIARowAccessible(nsIContent * aContent,DocAccessible * aDoc)452 ARIARowAccessible::ARIARowAccessible(nsIContent* aContent, DocAccessible* aDoc)
453     : HyperTextAccessibleWrap(aContent, aDoc) {
454   mGenericTypes |= eTableRow;
455 }
456 
NativeRole() const457 role ARIARowAccessible::NativeRole() const {
458   a11y::role r = GetAccService()->MarkupRole(mContent);
459   return r != roles::NOTHING ? r : roles::ROW;
460 }
461 
GroupPosition()462 GroupPos ARIARowAccessible::GroupPosition() {
463   int32_t count = 0, index = 0;
464   LocalAccessible* table = nsAccUtils::TableFor(this);
465   if (table) {
466     if (nsCoreUtils::GetUIntAttr(table->GetContent(), nsGkAtoms::aria_rowcount,
467                                  &count) &&
468         nsCoreUtils::GetUIntAttr(mContent, nsGkAtoms::aria_rowindex, &index)) {
469       return GroupPos(0, index, count);
470     }
471 
472     // Deal with the special case here that tables and grids can have rows
473     // which are wrapped in generic text container elements. Exclude tree grids
474     // because these are dealt with elsewhere.
475     if (table->Role() == roles::TABLE) {
476       LocalAccessible* row = nullptr;
477       AccIterator rowIter(table, filters::GetRow);
478       while ((row = rowIter.Next())) {
479         index++;
480         if (row == this) {
481           break;
482         }
483       }
484 
485       if (row) {
486         count = table->AsTable()->RowCount();
487         return GroupPos(0, index, count);
488       }
489     }
490   }
491 
492   return AccessibleWrap::GroupPosition();
493 }
494 
495 // LocalAccessible protected
NativeName(nsString & aName) const496 ENameValueFlag ARIARowAccessible::NativeName(nsString& aName) const {
497   // We want to calculate the name from content only if an ARIA role is
498   // present. ARIARowAccessible might also be used by tables with
499   // display:block; styling, in which case we do not want the name from
500   // content.
501   if (HasStrongARIARole()) {
502     return AccessibleWrap::NativeName(aName);
503   }
504 
505   return eNameOK;
506 }
507 
508 ////////////////////////////////////////////////////////////////////////////////
509 // ARIAGridCellAccessible
510 ////////////////////////////////////////////////////////////////////////////////
511 
512 ////////////////////////////////////////////////////////////////////////////////
513 // Constructor
514 
ARIAGridCellAccessible(nsIContent * aContent,DocAccessible * aDoc)515 ARIAGridCellAccessible::ARIAGridCellAccessible(nsIContent* aContent,
516                                                DocAccessible* aDoc)
517     : HyperTextAccessibleWrap(aContent, aDoc) {
518   mGenericTypes |= eTableCell;
519 }
520 
NativeRole() const521 role ARIAGridCellAccessible::NativeRole() const {
522   a11y::role r = GetAccService()->MarkupRole(mContent);
523   return r != roles::NOTHING ? r : roles::CELL;
524 }
525 
526 ////////////////////////////////////////////////////////////////////////////////
527 // TableCell
528 
Table() const529 TableAccessible* ARIAGridCellAccessible::Table() const {
530   LocalAccessible* table = nsAccUtils::TableFor(Row());
531   return table ? table->AsTable() : nullptr;
532 }
533 
ColIdx() const534 uint32_t ARIAGridCellAccessible::ColIdx() const {
535   LocalAccessible* row = Row();
536   if (!row) return 0;
537 
538   int32_t indexInRow = IndexInParent();
539   uint32_t colIdx = 0;
540   for (int32_t idx = 0; idx < indexInRow; idx++) {
541     LocalAccessible* cell = row->LocalChildAt(idx);
542     if (cell->IsTableCell()) {
543       colIdx += cell->AsTableCell()->ColExtent();
544     }
545   }
546 
547   return colIdx;
548 }
549 
RowIdx() const550 uint32_t ARIAGridCellAccessible::RowIdx() const { return RowIndexFor(Row()); }
551 
Selected()552 bool ARIAGridCellAccessible::Selected() {
553   LocalAccessible* row = Row();
554   if (!row) return false;
555 
556   return nsAccUtils::IsARIASelected(row) || nsAccUtils::IsARIASelected(this);
557 }
558 
559 ////////////////////////////////////////////////////////////////////////////////
560 // LocalAccessible
561 
ApplyARIAState(uint64_t * aState) const562 void ARIAGridCellAccessible::ApplyARIAState(uint64_t* aState) const {
563   HyperTextAccessibleWrap::ApplyARIAState(aState);
564 
565   // Return if the gridcell has aria-selected="true".
566   if (*aState & states::SELECTED) return;
567 
568   // Check aria-selected="true" on the row.
569   LocalAccessible* row = LocalParent();
570   if (!row || row->Role() != roles::ROW) return;
571 
572   nsIContent* rowContent = row->GetContent();
573   if (nsAccUtils::HasDefinedARIAToken(rowContent, nsGkAtoms::aria_selected) &&
574       !rowContent->AsElement()->AttrValueIs(kNameSpaceID_None,
575                                             nsGkAtoms::aria_selected,
576                                             nsGkAtoms::_false, eCaseMatters)) {
577     *aState |= states::SELECTABLE | states::SELECTED;
578   }
579 }
580 
NativeAttributes()581 already_AddRefed<AccAttributes> ARIAGridCellAccessible::NativeAttributes() {
582   RefPtr<AccAttributes> attributes =
583       HyperTextAccessibleWrap::NativeAttributes();
584 
585   // Expose "table-cell-index" attribute.
586   LocalAccessible* thisRow = Row();
587   if (!thisRow) return attributes.forget();
588 
589   int32_t rowIdx = RowIndexFor(thisRow);
590   if (rowIdx == -1) {  // error
591     return attributes.forget();
592   }
593 
594   int32_t colIdx = 0, colCount = 0;
595   uint32_t childCount = thisRow->ChildCount();
596   for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) {
597     LocalAccessible* child = thisRow->LocalChildAt(childIdx);
598     if (child == this) colIdx = colCount;
599 
600     roles::Role role = child->Role();
601     if (role == roles::CELL || role == roles::GRID_CELL ||
602         role == roles::ROWHEADER || role == roles::COLUMNHEADER) {
603       colCount++;
604     }
605   }
606 
607   attributes->SetAttribute(nsGkAtoms::tableCellIndex,
608                            rowIdx * colCount + colIdx);
609 
610 #ifdef DEBUG
611   RefPtr<nsAtom> cppClass = NS_Atomize(u"cppclass"_ns);
612   attributes->SetAttribute(cppClass, u"ARIAGridCellAccessible"_ns);
613 #endif
614 
615   return attributes.forget();
616 }
617 
GroupPosition()618 GroupPos ARIAGridCellAccessible::GroupPosition() {
619   int32_t count = 0, index = 0;
620   TableAccessible* table = Table();
621   if (table &&
622       nsCoreUtils::GetUIntAttr(table->AsAccessible()->GetContent(),
623                                nsGkAtoms::aria_colcount, &count) &&
624       nsCoreUtils::GetUIntAttr(mContent, nsGkAtoms::aria_colindex, &index)) {
625     return GroupPos(0, index, count);
626   }
627 
628   return GroupPos();
629 }
630