1 /*
   2  * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
   3  * 
   4  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   5  *
   6  * The contents of this file are subject to the terms of either the Universal Permissive License
   7  * v 1.0 as shown at http://oss.oracle.com/licenses/upl
   8  *
   9  * or the following license:
  10  *
  11  * Redistribution and use in source and binary forms, with or without modification, are permitted
  12  * provided that the following conditions are met:
  13  * 
  14  * 1. Redistributions of source code must retain the above copyright notice, this list of conditions
  15  * and the following disclaimer.
  16  * 
  17  * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
  18  * conditions and the following disclaimer in the documentation and/or other materials provided with
  19  * the distribution.
  20  * 
  21  * 3. Neither the name of the copyright holder nor the names of its contributors may be used to
  22  * endorse or promote products derived from this software without specific prior written permission.
  23  * 
  24  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
  25  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
  26  * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
  27  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  28  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  29  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
  30  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
  31  * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  32  */
  33 package org.openjdk.jmc.test.jemmy.misc.wrappers;
  34 
  35 import java.util.ArrayList;
  36 import java.util.HashMap;
  37 import java.util.List;
  38 import java.util.Map;
  39 
  40 import org.eclipse.swt.graphics.Image;
  41 import org.eclipse.swt.graphics.Rectangle;
  42 import org.eclipse.swt.widgets.Display;
  43 import org.eclipse.swt.widgets.Shell;
  44 import org.eclipse.swt.widgets.Table;
  45 import org.eclipse.swt.widgets.TableColumn;
  46 import org.eclipse.swt.widgets.TableItem;
  47 import org.jemmy.Point;
  48 import org.jemmy.control.Wrap;
  49 import org.jemmy.input.StringPopupOwner;
  50 import org.jemmy.input.StringPopupSelectableOwner;
  51 import org.jemmy.interfaces.Keyboard.KeyboardButtons;
  52 import org.jemmy.interfaces.Parent;
  53 import org.jemmy.interfaces.Selectable;
  54 import org.jemmy.lookup.Lookup;
  55 import org.jemmy.resources.StringComparePolicy;
  56 import org.jemmy.swt.ItemWrap;
  57 import org.jemmy.swt.TableWrap;
  58 import org.jemmy.swt.lookup.ByName;
  59 import org.junit.Assert;
  60 
  61 import org.openjdk.jmc.test.jemmy.misc.base.wrappers.MCJemmyBase;
  62 import org.openjdk.jmc.test.jemmy.misc.fetchers.Fetcher;
  63 
  64 /**
  65  * The Jemmy base wrapper for tables
  66  */
  67 public class MCTable extends MCJemmyBase {
  68 
  69         /**
  70          * A small representation of a row in a table, contains both the row text and a list of strings
  71          * representing all cells in the row. If no tests actually require this, we should change the
  72          * scope of this inner class to private or, at least, package private.
  73          */
  74         public class TableRow {
  75 
  76                 private final String text;
  77                 private final List<String> columnTexts;
  78                 private Map<String, Integer> columnNameMap;
  79 
  80                 TableRow(String text, List<String> columns, Map<String, Integer> columnNameMap) {
  81                         this.text = text;
  82                         columnTexts = columns;
  83                         this.columnNameMap = columnNameMap;
  84                 }
  85 
  86                 /**
  87                  * @param text
  88                  *            The text, separate from any column texts, to match
  89                  * @return {@code true} if the text matches that of this TableRow
  90                  */
  91                 boolean hasText(String text) {
  92                         return policy.compare(text, this.text);
  93                 }
  94 
  95                 /**
  96                  * @param text
  97                  *            the text, separate from any column texts, to match
  98                  * @param policy
  99                  *            the policy to use when matching
 100                  * @return {@code true} if the text matches that of this {@link TableRow}
 101                  */
 102                 boolean hasText(String text, StringComparePolicy policy) {
 103                         return policy.compare(text, this.text);
 104                 }
 105 
 106                 /**
 107                  * @param text
 108                  *            the text to be found
 109                  * @return whether or not the text has been found in any column
 110                  */
 111                 boolean hasColumnText(String text) {
 112                         return hasColumnText(text, policy);
 113                 }
 114 
 115                 /**
 116                  * @param text
 117                  *            the text to be found
 118                  * @param policy
 119                  *            the policy to use when matching
 120                  * @return whether or not the text has been found in any column
 121                  */
 122                 boolean hasColumnText(String text, StringComparePolicy policy) {
 123                         for (String col : columnTexts) {
 124                                 if (policy.compare(text, col)) {
 125                                         return true;
 126                                 }
 127                         }
 128                         return false;
 129                 }
 130 
 131                 /**
 132                  * @return the text of a row.
 133                  */
 134                 public String getText() {
 135                         return text;
 136                 }
 137 
 138                 /**
 139                  * Returns the row text for the provided column index
 140                  *
 141                  * @param columnIndex
 142                  *            the column index
 143                  * @return the text of the field of the provided column
 144                  */
 145                 public String getText(int columnIndex) {
 146                         return columnTexts.get(columnIndex);
 147                 }
 148 
 149                 /**
 150                  * Returns the row text for the provided column header
 151                  *
 152                  * @param columnHeader
 153                  *            the string header of the column
 154                  * @return the text of the field of the provided column
 155                  */
 156                 public String getText(String columnHeader) {
 157                         return columnTexts.get(columnNameMap.get(columnHeader));
 158                 }
 159 
 160                 /**
 161                  * @return the texts in the columns of a row
 162                  */
 163                 public List<String> getColumns() {
 164                         return columnTexts;
 165                 }
 166 
 167                 @Override
 168                 public String toString() {
 169                         StringBuilder sb = new StringBuilder();
 170                         sb.append(text);
 171                         sb.append(":[");
 172                         for (String col : columnTexts) {
 173                                 sb.append(col);
 174                                 sb.append(' ');
 175                         }
 176                         sb.append("]");
 177                         return sb.toString();
 178                 }
 179 
 180                 @Override
 181                 public boolean equals(Object o) {
 182                         if (!(o instanceof TableRow)) {
 183                                 return false;
 184                         }
 185                         return toString().equals(((TableRow) o).toString());
 186                 }
 187 
 188                 public Map<String, Integer> getColumnNameMap() {
 189                         return columnNameMap;
 190                 }
 191         }
 192 
 193         /**
 194          * The policy used in comparisons in McTables
 195          */
 196         public static StringComparePolicy policy = StringComparePolicy.SUBSTRING;
 197 
 198         private MCTable(Wrap<? extends Table> tableWrap) {
 199                 this.control = tableWrap;
 200         }
 201 
 202         /**
 203          * @return a list of all the tables in the default shell.
 204          */
 205         public static List<MCTable> getAll() {
 206                 return getAll(getShell());
 207         }
 208 
 209         /**
 210          * Returns all currently visible tables as McTables in a list.
 211          *
 212          * @param shell
 213          *            the shell to search for tables.
 214          * @return a {@link List} of {@link MCTable}
 215          */
 216         public static List<MCTable> getAll(Wrap<? extends Shell> shell) {
 217                 return getAll(shell, true);
 218         }
 219 
 220         /**
 221          * Returns all currently visible tables as McTables in a list.
 222          *
 223          * @param shell
 224          *            the shell to search for tables.
 225          * @param waitForIdle
 226          *            {@code true} if supposed to wait for the UI to be idle before performing the
 227          *            lookup
 228          * @return a {@link List} of {@link MCTable}
 229          */
 230         @SuppressWarnings("unchecked")
 231         public static List<MCTable> getAll(Wrap<? extends Shell> shell, boolean waitForIdle) {
 232                 List<Wrap<? extends Table>> list = getVisible(shell.as(Parent.class, Table.class).lookup(Table.class),
 233                                 waitForIdle, false);
 234                 List<MCTable> tables = new ArrayList<>();
 235                 for (int i = 0; i < list.size(); i++) {
 236                         tables.add(new MCTable(list.get(i)));
 237                 }
 238                 return tables;
 239         }
 240 
 241         /**
 242          * Returns all currently visible tables as {@link MCTable} in a list.
 243          *
 244          * @param dialog
 245          *            the {@link MCDialog} to search for tables.
 246          * @return a {@link List} of {@link MCTable}
 247          */
 248         public static List<MCTable> getAll(MCDialog dialog) {
 249                 return getAll(dialog.getDialogShell());
 250         }
 251 
 252         /**
 253          * Finds tables by index, generally you should not use this method, but rather get all tables
 254          * and keep the list up-to-date.
 255          *
 256          * @param shell
 257          *            the shell to search
 258          * @param index
 259          *            the index in the list of tables
 260          * @return the {@link MCTable} representing the table at the specified index, or {@code null}
 261          *         if index is out of range
 262          */
 263         @SuppressWarnings("unchecked")
 264         static MCTable getByIndex(Wrap<? extends Shell> shell, int index) {
 265                 Lookup<Table> lookup = shell.as(Parent.class, Table.class).lookup(Table.class);
 266                 return (index < lookup.size()) ? new MCTable(lookup.wrap(index)) : null;
 267         }
 268 
 269         /**
 270          * Finds tables by column header (first match only)
 271          *
 272          * @param headerName
 273          *            the name of the column header
 274          * @return a {@link MCTable}
 275          */
 276         public static MCTable getByColumnHeader(String headerName) {
 277                 return getByColumnHeader(getShell(), headerName);
 278         }
 279 
 280         /**
 281          * Finds tables by column header (first match only)
 282          *
 283          * @param shell
 284          *            the shell in which to look for the table
 285          * @param headerName
 286          *            the name of the column header
 287          * @return a {@link MCTable}
 288          */
 289         public static MCTable getByColumnHeader(Wrap<? extends Shell> shell, String headerName) {
 290                 for (MCTable table : getAll(shell)) {
 291                         if (table.getColumnIndex(headerName) != null) {
 292                                 return table;
 293                         }
 294                 }
 295                 return null;
 296         }
 297 
 298         /**
 299          * Finds a table by name (data set by the key "name")
 300          *
 301          * @param name
 302          *            the name of the table
 303          * @return a {@link MCTable}
 304          */
 305         public static MCTable getByName(String name) {
 306                 return getByName(getShell(), name);
 307         }
 308 
 309         /**
 310          * Finds a table by name (data set by the key "name") that is child of the provided dialog
 311          *
 312          * @param dialog
 313          *            the dialog from where to start the search (ancestor)
 314          * @param name
 315          *            the name of the table
 316          * @return a {@link MCTable}
 317          */
 318         public static MCTable getByName(MCDialog dialog, String name) {
 319                 return getByName(dialog.getDialogShell(), name);
 320         }
 321 
 322         /**
 323          * Finds a table by name (data set by the key "name") that is child of the provided shell
 324          *
 325          * @param shell
 326          *            the shell from where to start the search (ancestor)
 327          * @param name
 328          *            the name of the table
 329          * @return a {@link MCTable}
 330          */
 331         @SuppressWarnings("unchecked")
 332         public static MCTable getByName(Wrap<? extends Shell> shell, String name) {
 333                 return new MCTable(shell.as(Parent.class, Table.class)
 334                                 .lookup(Table.class, new ByName<>(name, StringComparePolicy.EXACT)).wrap());
 335         }
 336 
 337         /**
 338          * Returns a List of string lists containing the table's complete table item text values.
 339          *
 340          * @return a {@link List} of {@link List} of {@link String}
 341          */
 342         public List<List<String>> getAllColumnItemTexts() {
 343                 List<List<String>> result = new ArrayList<>();
 344                 for (TableRow tableRow : getRows()) {
 345                         result.add(tableRow.getColumns());
 346                 }
 347                 return result;
 348         }
 349 
 350         /**
 351          * Returns a column from a table
 352          *
 353          * @param columnId
 354          *            the column to get
 355          * @return the requested column's text value(s)
 356          */
 357         public List<String> getColumnItemTexts(int columnId) {
 358                 List<String> column = new ArrayList<>();
 359                 for (TableRow row : getRows()) {
 360                         column.add(row.getText(columnId));
 361                 }
 362                 return column;
 363         }
 364 
 365         /**
 366          * Returns a column from a table
 367          *
 368          * @param columnHeader
 369          *            the column to get
 370          * @return the requested column's text value(s)
 371          */
 372         public List<String> getColumnItemTexts(String columnHeader) {
 373                 List<String> column = new ArrayList<>();
 374                 for (TableRow row : getRows()) {
 375                         column.add(row.getText(columnHeader));
 376                 }
 377                 return column;
 378         }
 379 
 380         /**
 381          * @param columnHeader
 382          *            the header of the column
 383          * @return the index of the column
 384          */
 385         public Integer getColumnIndex(String columnHeader) {
 386                 return getColumnNameMap().get(columnHeader);
 387         }
 388 
 389         private Map<String, Integer> getColumnNameMap() {
 390                 final Table table = getWrap().getControl();
 391                 Fetcher<Map<String, Integer>> fetcher = new Fetcher<Map<String, Integer>>() {
 392                         @Override
 393                         public void run() {
 394                                 TableColumn[] tableColumns = table.getColumns();
 395                                 Map<String, Integer> columnNameMap = new HashMap<>();
 396                                 int columnIndex = 0;
 397                                 for (TableColumn tc : tableColumns) {
 398                                         columnNameMap.put(tc.getText(), columnIndex);
 399                                         columnIndex++;
 400                                 }
 401                                 setOutput(columnNameMap);
 402                         }
 403                 };
 404                 Display.getDefault().syncExec(fetcher);
 405                 return fetcher.getOutput();
 406         }
 407 
 408         /**
 409          * Returns a list of strings for the table item of the specified index.
 410          *
 411          * @param rowIndex
 412          *            the index of the item to get the text for
 413          * @return a {@link List} of {@link String}
 414          */
 415         public List<String> getItemTexts(int rowIndex) {
 416                 TableRow row = getRow(rowIndex);
 417                 return row.getColumns();
 418         }
 419 
 420         /**
 421          * Gets a TableRow for the row index provided.
 422          *
 423          * @param index
 424          *            the index of the row to get data from
 425          * @return a {@link TableRow} with the data from the table row
 426          */
 427         public TableRow getRow(int index) {
 428                 return getRow(index, getColumnNameMap());
 429         }
 430 
 431         /**
 432          * Gets a TableRow for the row index provided.
 433          *
 434          * @param index
 435          *            the index of the row to get data from
 436          * @param columnNameMap
 437          *            a map of the columns' headers and indexes
 438          * @return a {@link TableRow} with the data from the table row
 439          */
 440         public TableRow getRow(int index, Map<String, Integer> columnNameMap) {
 441                 final Table table = getWrap().getControl();
 442                 Fetcher<TableRow> fetcher = new Fetcher<TableRow>() {
 443                         @Override
 444                         public void run() {
 445                                 int columns = columnNameMap.size();
 446                                 TableRow output;
 447                                 TableItem item = table.getItem(index);
 448                                 String text = item.getText();
 449                                 List<String> texts = new ArrayList<>();
 450                                 for (int i = 0; i < columns; i++) {
 451                                         texts.add(item.getText(i));
 452                                 }
 453                                 output = new TableRow(text, texts, columnNameMap);
 454                                 setOutput(output);
 455                         }
 456                 };
 457                 Display.getDefault().syncExec(fetcher);
 458                 return fetcher.getOutput();
 459         }
 460 
 461         /**
 462          * Gets all the row and column data of the table
 463          *
 464          * @return a {@link List} of {@link TableRow}
 465          */
 466         public List<TableRow> getRows() {
 467                 int numberOfItems = this.getItemCount();
 468                 List<TableRow> allRows = new ArrayList<>();
 469 
 470                 Map<String, Integer> columnNameMap = getColumnNameMap();
 471                 for (int i = 0; i < numberOfItems; i++) {
 472                         allRows.add(getRow(i, columnNameMap));
 473                 }
 474 
 475                 return allRows;
 476         }
 477 
 478         /**
 479          * Gets an Image for a specific row of the table
 480          *
 481          * @param rowIndex
 482          *            index of the row to get
 483          * @return an {@link Image}
 484          */
 485         public Image getItemImage(int rowIndex) {
 486                 final Table table = getWrap().getControl();
 487                 Fetcher<Image> fetcher = new Fetcher<Image>() {
 488                         @Override
 489                         public void run() {
 490                                 TableItem item = table.getItem(rowIndex);
 491                                 Image icon = item.getImage();
 492                                 setOutput(icon);
 493                         }
 494                 };
 495                 Display.getDefault().syncExec(fetcher);
 496                 return fetcher.getOutput();
 497         }
 498 
 499         /**
 500          * Gets the number of items in the table
 501          *
 502          * @return the number of items in the table
 503          */
 504         public int getItemCount() {
 505                 final Table table = getWrap().getControl();
 506                 Fetcher<Integer> fetcher = new Fetcher<Integer>() {
 507                         @Override
 508                         public void run() {
 509                                 int count = table.getItemCount();
 510                                 setOutput(count);
 511                         }
 512                 };
 513                 Display.getDefault().syncExec(fetcher);
 514                 return fetcher.getOutput().intValue();
 515         }
 516 
 517         /**
 518          * Whether or not the table contains the text given
 519          *
 520          * @param item
 521          *            the text
 522          * @return {@code true} if found.
 523          */
 524         public boolean hasItem(String item) {
 525                 return (getItemIndex(item) != -1) ? true : false;
 526         }
 527 
 528         /**
 529          * Returns the number of (exactly) matching table items
 530          *
 531          * @param itemText
 532          *            the text
 533          * @return the number of matching items in the table
 534          */
 535         public int numberOfMatchingItems(String itemText) {
 536                 return numberOfMatchingItems(itemText, StringComparePolicy.EXACT);
 537         }
 538 
 539         /**
 540          * Returns the number of matching table items
 541          *
 542          * @param itemText
 543          *            the text of the items to match
 544          * @param policy
 545          *            the policy to use when matching
 546          * @return the number of matching items in the table
 547          */
 548         public int numberOfMatchingItems(String itemText, StringComparePolicy policy) {
 549                 return getItemIndexes(itemText, policy).size();
 550         }
 551 
 552         /**
 553          * Returns the indexes of matching table items (Exact matching)
 554          *
 555          * @param itemText
 556          *            the text of the items to match
 557          * @return a {@link List} of {@link Integer} of the matching indexes
 558          */
 559         public List<Integer> getItemIndexes(String itemText) {
 560                 return getItemIndexes(itemText, StringComparePolicy.EXACT);
 561         }
 562 
 563         /**
 564          * Returns the indexes of matching table items
 565          *
 566          * @param itemText
 567          *            the text of the matching table item
 568          * @param policy
 569          *            the matching policy to use
 570          * @return a {@link List} of {@link Integer} of the matching indexes
 571          */
 572         public List<Integer> getItemIndexes(String itemText, StringComparePolicy policy) {
 573                 List<TableRow> rows = getRows();
 574                 List<Integer> index = new ArrayList<>();
 575                 for (int i = 0; i < rows.size(); i++) {
 576                         TableRow row = rows.get(i);
 577                         if (row.hasColumnText(itemText, policy) || row.hasText(itemText, policy)) {
 578                                 index.add(i);
 579                         }
 580                 }
 581                 return index;
 582         }
 583 
 584         /**
 585          * Selects the given item (if found). This could also be done using the Selector of the Table
 586          * (like "tableWrap.as(Selectable.class).selector().select(goalIndex)") but there seems to be an
 587          * issue with TableItem.getBounds() on OS X where we run into some nasty ArrayIndexOutOfBounds
 588          * exceptions because that code relies on mouse().click(). Another drawback with that approach
 589          * is that we might actually be trying to click outside of what's visible. Keyboard navigation
 590          * is safer so the Jemmy IndexItemSelector class (as well as TextItemSelector) should be fixed
 591          * to do that instead
 592          *
 593          * @param item
 594          *            the item to select
 595          */
 596         public void select(String item) {
 597                 Assert.assertTrue("Unable to select " + item + ".", select(getItemIndex(item)));
 598         }
 599 
 600         /**
 601          * Selects the given item (if found). This could also be done using the Selector of the Table
 602          * (like "tableWrap.as(Selectable.class).selector().select(goalIndex)") but there seems to be an
 603          * issue with TableItem.getBounds() on OS X where we run into some nasty ArrayIndexOutOfBounds
 604          * exceptions because that code relies on mouse().click(). Another drawback with that approach
 605          * is that we might actually be trying to click outside of what's visible. Keyboard navigation
 606          * is safer so the Jemmy IndexItemSelector class (as well as TextItemSelector) should be fixed
 607          * to do that instead
 608          *
 609          * @param item
 610          *            the item to select
 611          * @param columnIndex
 612          *            the column index to select
 613          */
 614         public void select(String item, int columnIndex) {
 615                 Assert.assertTrue("Unable to select " + item + ".", select(getItemIndex(item), columnIndex));
 616         }
 617 
 618         /**
 619          * Performs a mouse click at a specified column index of an item
 620          * 
 621          * @param item
 622          *            the item to click
 623          * @param columnIndex
 624          *            the column index where to click
 625          */
 626         public void clickItem(String item, int columnIndex) {
 627                 select(getItemIndex(item), columnIndex);
 628                 scrollbarSafeSelection();
 629                 control.mouse().click(1, getRelativeClickPoint(getSelectedItem(), columnIndex));
 630         }
 631 
 632         /**
 633          * Performs a mouse click at a specified column header's index of an item
 634          * 
 635          * @param item
 636          *            the item to click
 637          * @param columnHeader
 638          *            the column header
 639          */
 640         public void clickItem(String item, String columnHeader) {
 641                 clickItem(item, getColumnIndex(columnHeader));
 642         }
 643 
 644         /**
 645          * Selects the given item (if found). This could also be done using the Selector of the Table
 646          * (like "tableWrap.as(Selectable.class).selector().select(goalIndex)") but there seems to be an
 647          * issue with TableItem.getBounds() on OS X where we run into ArrayIndexOutOfBounds exceptions
 648          * because that code relies on mouse().click(). Another drawback with that approach is that we
 649          * might actually be trying to click outside of what's visible. Keyboard navigation is safer so
 650          * the Jemmy IndexItemSelector class (as well as TextItemSelector) should be fixed to do that
 651          * instead
 652          *
 653          * @param item
 654          *            the item to select
 655          * @param columnHeader
 656          *            the column header to select
 657          */
 658         public void select(String item, String columnHeader) {
 659                 Assert.assertTrue("Unable to select " + item + ".", select(getItemIndex(item), getColumnIndex(columnHeader)));
 660         }
 661 
 662         /**
 663          * Selects the given item (if found). This could also be done using the Selector of the Table
 664          * (like "tableWrap.as(Selectable.class).selector().select(goalIndex)") but there seems to be an
 665          * issue with TableItem.getBounds() on OS X where we run into ArrayIndexOutOfBounds exceptions
 666          * because that code relies on mouse().click(). Another drawback with that approach is that we
 667          * might actually be trying to click outside of what's visible. Keyboard navigation is safer so
 668          * the Jemmy IndexItemSelector class (as well as TextItemSelector) should be fixed to do that
 669          * instead
 670          *
 671          * @param item
 672          *            the item to select
 673          * @param exactMatching
 674          *            if {@code true} {@link StringComparePolicy.EXACT} is used. Otherwise
 675          *            {@link StringComparePolicy.SUBSTRING} will be used
 676          */
 677         public void select(String item, boolean exactMatching) {
 678                 StringComparePolicy thisPolicy = (exactMatching) ? StringComparePolicy.EXACT : StringComparePolicy.SUBSTRING;
 679                 Assert.assertTrue("Unable to select " + item + ".", select(getItemIndex(item, thisPolicy)));
 680         }
 681 
 682         /**
 683          * Selects the item at the given index (if not -1)). Will retry the selection a maximum number
 684          * of three times just to make sure that lost and regained focus doesn't break things
 685          *
 686          * @param index
 687          *            the index of the item
 688          * @param columnIndex
 689          *            the column index of the item to select
 690          * @return {@code true} if selected index is the same as the provided. {@code false} otherwise
 691          */
 692         public boolean select(int index, int columnIndex) {
 693                 if (index != -1) {
 694                         ensureFocus();
 695                         int maxRetries = 3;
 696                         while (control.getProperty(Integer.class, Selectable.STATE_PROP_NAME) != index && maxRetries > 0) {
 697                                 maxRetries--;
 698                                 int startIndex = control.getProperty(Integer.class, Selectable.STATE_PROP_NAME);
 699                                 if (startIndex == -1) {
 700                                         control.keyboard().pushKey(KeyboardButtons.DOWN);
 701                                         control.keyboard().pushKey(KeyboardButtons.UP);
 702                                         startIndex = control.getProperty(Integer.class, Selectable.STATE_PROP_NAME);
 703                                 }
 704                                 if (startIndex != -1) {
 705                                         int steps = index - startIndex;
 706                                         KeyboardButtons stepButton = (index > startIndex) ? KeyboardButtons.DOWN : KeyboardButtons.UP;
 707                                         for (int i = 0; i < Math.abs(steps); i++) {
 708                                                 control.keyboard().pushKey(stepButton);
 709                                         }
 710                                         // if we have a column > 0 do some side stepping
 711                                         for (int i = 0; i < columnIndex; i++) {
 712                                                 control.keyboard().pushKey(KeyboardButtons.RIGHT);
 713                                         }
 714                                 }
 715                         }
 716                         return (control.getProperty(Integer.class, Selectable.STATE_PROP_NAME) == index && index != -1);
 717                 } else {
 718                         return false;
 719                 }
 720         }
 721 
 722         /**
 723          * Selects the item at the given index (if not -1))
 724          *
 725          * @param index
 726          *            the index of the item
 727          * @return {@code true} if selected index is the same as the provided. {@code false} otherwise
 728          */
 729         public boolean select(int index) {
 730                 return select(index, 0);
 731         }
 732 
 733         /**
 734          * Context clicks the currently selected table item and chooses the supplied option
 735          *
 736          * @param desiredState
 737          *            the selection state to which the context choice is to be to set to
 738          * @param choice
 739          *            the context menu path to the option
 740          */
 741         @SuppressWarnings("unchecked")
 742         public void contextChoose(boolean desiredState, String ... choice) {
 743                 scrollbarSafeSelection();
 744                 StringPopupSelectableOwner<Table> spo = control.as(StringPopupSelectableOwner.class);
 745                 spo.setPolicy(policy);
 746                 spo.push(desiredState, getRelativeClickPoint(getSelectedItem()), choice);
 747         }
 748 
 749         /**
 750          * Context clicks the currently selected table item and finds out the selection status of the
 751          * supplied option
 752          *
 753          * @param choice
 754          *            the context menu path to the option
 755          * @return the selection status of the item
 756          */
 757         @SuppressWarnings("unchecked")
 758         public boolean getContextOptionState(String ... choice) {
 759                 scrollbarSafeSelection();
 760                 StringPopupSelectableOwner<Table> spo = control.as(StringPopupSelectableOwner.class);
 761                 spo.setPolicy(policy);
 762                 return spo.getState(getRelativeClickPoint(getSelectedItem()), choice);
 763         }
 764 
 765         /**
 766          * Context clicks the currently selected table item and chooses the supplied option
 767          *
 768          * @param choice
 769          *            the context menu path to the option
 770          */
 771         @SuppressWarnings("unchecked")
 772         public void contextChoose(String ... choice) {
 773                 scrollbarSafeSelection();
 774                 StringPopupOwner<Table> spo = control.as(StringPopupOwner.class);
 775                 spo.setPolicy(policy);
 776                 spo.push(getRelativeClickPoint(getSelectedItem()), choice);
 777         }
 778 
 779         private Wrap<? extends TableItem> getSelectedItem() {
 780                 Fetcher<TableItem> fetcher = new Fetcher<TableItem>() {
 781                         @Override
 782                         public void run() {
 783                                 setOutput(getWrap().getControl().getSelection()[0]);
 784                         }
 785                 };
 786                 Display.getDefault().syncExec(fetcher);
 787                 return new ItemWrap<>(getWrap(), fetcher.getOutput());
 788         }
 789 
 790         /**
 791          * Calculates the click point of the child relative to the parent provided. Uses a rather
 792          * cumbersome way of getting the bounds because {@link ArrayIndexOutOfBoundsException} in some
 793          * cases getting thrown on Mac OS X.
 794          *
 795          * @param child
 796          *            the wrapped child control
 797          * @return the {@link Point} of the child relative to the parent
 798          */
 799         private Point getRelativeClickPoint(final Wrap<? extends TableItem> child) {
 800                 return getRelativeClickPoint(child, null);
 801         }
 802 
 803         /**
 804          * Calculates the click point of the child relative to the parent. Uses a rather cumbersome way
 805          * of getting the bounds because {@link ArrayIndexOutOfBoundsException} in some cases getting
 806          * thrown on Mac OS X.
 807          *
 808          * @param child
 809          *            the wrapped child control
 810          * @param columnIndex
 811          *            the column index of the table item for which to get the click point. May be null
 812          *            if no column
 813          * @return the {@link Point} of the child relative to the parent
 814          */
 815         private Point getRelativeClickPoint(final Wrap<? extends TableItem> child, final Integer columnIndex) {
 816                 Fetcher<Point> fetcher = new Fetcher<Point>() {
 817                         @Override
 818                         public void run() {
 819                                 Rectangle childRect = null;
 820                                 if (columnIndex != null) {
 821                                         childRect = child.getControl().getBounds(columnIndex);
 822                                 } else {
 823                                         try {
 824                                                 childRect = child.getControl().getBounds();
 825                                         } catch (ArrayIndexOutOfBoundsException e) {
 826                                                 childRect = child.getControl().getBounds(0);
 827                                         }
 828                                 }
 829                                 setOutput(new Point(childRect.x + childRect.width / 2, childRect.y + childRect.height / 2));
 830                         }
 831                 };
 832                 Display.getDefault().syncExec(fetcher);
 833                 return fetcher.getOutput();
 834         }
 835 
 836         private int getItemIndex(String itemText) {
 837                 return getItemIndex(itemText, policy);
 838         }
 839 
 840         private int getItemIndex(String itemText, StringComparePolicy policy) {
 841                 List<TableRow> rows = getRows();
 842                 int index = -1;
 843                 for (int i = 0; i < rows.size(); i++) {
 844                         TableRow row = rows.get(i);
 845                         if (row.hasColumnText(itemText, policy) || row.hasText(itemText, policy)) {
 846                                 index = i;
 847                                 break;
 848                         }
 849                 }
 850                 return index;
 851         }
 852 
 853         @SuppressWarnings("unchecked")
 854         private Wrap<? extends Table> getWrap() {
 855                 return control.as(TableWrap.class);
 856         }
 857 
 858         private void scrollbarSafeSelection() {
 859                 int index = control.getProperty(Integer.class, Selectable.STATE_PROP_NAME);
 860                 control.keyboard().pushKey(KeyboardButtons.DOWN);
 861                 control.keyboard().pushKey(KeyboardButtons.UP);
 862                 select(index);
 863         }
 864 }