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