/* * Copyright 2016 Nokia Solutions and Networks * Licensed under the Apache License, Version 2.0, * see license.txt file for details. */ package org.robotframework.ide.eclipse.main.plugin.hyperlink; import static com.google.common.collect.Lists.newArrayList; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Optional; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.layout.GridLayoutFactory; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.Region; import org.eclipse.jface.text.hyperlink.IHyperlink; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.StyledString; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.ViewerColumnsFactory; import org.eclipse.nebula.widgets.nattable.NatTable; import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.MouseAdapter; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseMoveListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Cursor; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.TableItem; import org.robotframework.ide.eclipse.main.plugin.hyperlink.detectors.ITableHyperlinksDetector; import org.robotframework.red.nattable.TableCellStringData; import org.robotframework.red.nattable.TableCellsStrings; import org.robotframework.red.swt.SwtThread; import org.robotframework.red.viewers.RedCommonLabelProvider; import org.robotframework.red.viewers.Selections; import org.robotframework.red.viewers.StructuredContentProvider; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Iterables; import com.google.common.collect.Range; public class TableHyperlinksSupport { private final NatTable table; private final TableCellsStrings tableStrings; private final List<ITableHyperlinksDetector> detectors = new ArrayList<>(); private Shell infoShell; private List<IHyperlink> hyperlinks = new ArrayList<>(); private TableCellStringData currentData = null; private Cursor cursor = null; public static TableHyperlinksSupport enableHyperlinksInTable(final NatTable table, final TableCellsStrings tableStrings) { final TableHyperlinksSupport detector = new TableHyperlinksSupport(table, tableStrings); table.addMouseListener(detector.new HyperlinksClickListener()); table.addMouseMoveListener(detector.new HyperlinksMouseMoveListener()); final Display display = table.getDisplay(); final HyperlinksKeyListener filter = detector.new HyperlinksKeyListener(); display.addFilter(SWT.KeyUp, filter); table.addDisposeListener(new DisposeListener() { @Override public void widgetDisposed(final DisposeEvent e) { display.removeFilter(SWT.KeyUp, filter); } }); return detector; } private TableHyperlinksSupport(final NatTable table, final TableCellsStrings tableStrings) { this.tableStrings = tableStrings; this.table = table; } public void addDetectors(final ITableHyperlinksDetector... detectors) { this.detectors.addAll(newArrayList(detectors)); } public void removeDetector(final ITableHyperlinksDetector detector) { this.detectors.remove(detector); } private void removeHyperlink() { if (infoShell != null && !infoShell.isDisposed()) { infoShell.close(); infoShell.dispose(); infoShell = null; } if (cursor != null) { table.getShell().setCursor(null); cursor.dispose(); cursor = null; } if (currentData != null) { currentData.removeHyperlink(); table.redraw(); } hyperlinks.clear(); } private void openHyperlink(final IHyperlink linkToOpen) { removeHyperlink(); SwtThread.asyncExec(new Runnable() { @Override public void run() { linkToOpen.open(); } }); } public List<ITableHyperlinksDetector> getDetectors() { return detectors; } @VisibleForTesting static Optional<IRegion> getMergedHyperlinkRegion(final Collection<IHyperlink> hyperlinks) { if (hyperlinks.isEmpty()) { return Optional.empty(); } IRegion hyperlinkRegion = Iterables.getFirst(hyperlinks, null).getHyperlinkRegion(); for (final IHyperlink link : hyperlinks) { hyperlinkRegion = merge(hyperlinkRegion, link.getHyperlinkRegion()); } return Optional.of(hyperlinkRegion); } private static IRegion merge(final IRegion region1, final IRegion region2) { final int startOffset = Integer.min(region1.getOffset(), region2.getOffset()); final int endOffset = Integer.max(region1.getOffset() + region1.getLength(), region2.getOffset() + region2.getLength()); return new Region(startOffset, endOffset - startOffset); } private class HyperlinksKeyListener implements Listener { @Override public void handleEvent(final Event event) { if (event.keyCode == SWT.CTRL) { removeHyperlink(); } } } private class HyperlinksClickListener extends MouseAdapter { @Override public void mouseUp(final MouseEvent e) { if (!hyperlinks.isEmpty()) { openHyperlink(hyperlinks.get(0)); } } } private class HyperlinksMouseMoveListener implements MouseMoveListener { @Override public void mouseMove(final MouseEvent e) { if (e.stateMask != SWT.CTRL || detectors.isEmpty()) { // no detectors or CTRL is not pressed removeHyperlink(); return; } final int column = table.getColumnPositionByX(e.x); final int row = table.getRowPositionByY(e.y); final ILayerCell cell = table.getCellByPosition(column, row); if (cell == null) { // no cell for given table coordinates removeHyperlink(); return; } final String actualLabel = (String) cell.getDataValue(); final TableCellStringData textData = tableStrings.get(column, row); if (textData == null) { // no info about labels drawn in this cell removeHyperlink(); return; } final int index = textData.getCharacterIndexFrom(e.x, e.y); if (index < 0) { // mouse position is outside of drawn label if (isPopupOpen()) { final Point popupLocation = infoShell.getLocation(); final Point popupSize = infoShell.getSize(); final Rectangle popupRectangle = new Rectangle(popupLocation.x, popupLocation.y, popupSize.x, popupSize.y); final Point labelLocation = table.toDisplay(textData.getCoordinate()); final Rectangle labelRectangle = new Rectangle(labelLocation.x, labelLocation.y, textData.getExtent().x, textData.getExtent().y); if (!popupRectangle.union(labelRectangle).contains(table.toDisplay(e.x, e.y))) { // mouse is moving outside of popup, so we need to close and remove removeHyperlink(); } } else { removeHyperlink(); } return; } final Range<Integer> currentHyperlinkRegion = textData.getHyperlinkRegion(); if (currentHyperlinkRegion != null && currentHyperlinkRegion.lowerEndpoint() <= index && index <= currentHyperlinkRegion.upperEndpoint()) { // no need to remove hyperlinks, we're moving inside place which already has link return; } else if (currentHyperlinkRegion != null && isPopupOpen()) { // we're over the label, outside the generated link, but the popup is open, so we // don't want to recalculate hyperlinks return; } hyperlinks = collectHyperlinks(column, row, actualLabel, index); final Optional<IRegion> hyperlinkRegion = getMergedHyperlinkRegion(hyperlinks); if (!hyperlinkRegion.isPresent()) { // there is no hyperlink region removeHyperlink(); return; } if (currentData != null) { currentData.removeHyperlink(); } textData.createHyperlinkAt(hyperlinkRegion.get().getOffset(), hyperlinkRegion.get().getOffset() + hyperlinkRegion.get().getLength()); currentData = textData; if (hyperlinks.size() > 1) { openChoicePopup(calculatePopupLocation(cell, textData)); } changeCursor(); table.redraw(); } private boolean isPopupOpen() { return infoShell != null && !infoShell.isDisposed() && infoShell.isVisible(); } private List<IHyperlink> collectHyperlinks(final int column, final int row, final String actualLabel, final int index) { // 1 is substracted due to column/row headers final List<IHyperlink> hyperlinks = new ArrayList<>(); for (final ITableHyperlinksDetector detector : detectors) { hyperlinks.addAll(detector.detectHyperlinks(row - 1, column - 1, actualLabel, index)); } return hyperlinks; } private Point calculatePopupLocation(final ILayerCell cell, final TableCellStringData textData) { final int x = textData.getCoordinate().x; final int y = cell.getBounds().y + cell.getBounds().height; return table.toDisplay(x, y); } private void openChoicePopup(final Point location) { if (infoShell != null && !infoShell.isDisposed()) { infoShell.close(); infoShell.dispose(); } infoShell = new Shell(table.getShell(), SWT.TOOL | SWT.ON_TOP); infoShell.setLocation(location); GridLayoutFactory.fillDefaults().applyTo(infoShell); final Composite comp = new Composite(infoShell, SWT.NONE); comp.setBackground(table.getBackground()); GridLayoutFactory.fillDefaults().margins(3, 5).applyTo(comp); GridDataFactory.fillDefaults().grab(true, false).applyTo(comp); final TableViewer viewer = new TableViewer(comp, SWT.SINGLE | SWT.NO_SCROLL); viewer.getTable().setHeaderVisible(false); viewer.getTable().setLinesVisible(false); viewer.setContentProvider(new HyperlinksContentProvider()); viewer.getTable().addMouseMoveListener(new MouseMoveListener() { @Override public void mouseMove(final MouseEvent e) { if (viewer.getTable().equals(e.getSource())) { final TableItem item = viewer.getTable().getItem(new Point(e.x, e.y)); if (item != null) { viewer.getTable().setSelection(new TableItem[] { item }); } } } }); viewer.getTable().addMouseListener(new MouseAdapter() { @Override public void mouseUp(final MouseEvent e) { if (viewer.getTable().getSelectionCount() < 1 || e.button != 1) { return; } if (viewer.getTable().equals(e.getSource())) { final TableItem item = viewer.getTable().getItem(new Point(e.x, e.y)); final TableItem selection = viewer.getTable().getSelection()[0]; if (selection.equals(item)) { openHyperlink((IHyperlink) item.getData()); } } } }); viewer.getTable().addSelectionListener(new SelectionAdapter() { @Override public void widgetDefaultSelected(final SelectionEvent e) { openHyperlink(Selections.getSingleElement((IStructuredSelection) viewer.getSelection(), IHyperlink.class)); } }); ViewerColumnsFactory.newColumn("") .labelsProvidedBy(new HyperlinksLabelProvider()) .shouldGrabAllTheSpaceLeft(true) .withMinWidth(50) .createFor(viewer); viewer.setInput(hyperlinks); GridDataFactory.fillDefaults().grab(true, true).applyTo(viewer.getTable()); viewer.getTable().select(0); viewer.getTable().getColumn(0).pack(); viewer.getTable().pack(); infoShell.pack(); infoShell.setVisible(true); } private void changeCursor() { if (cursor == null) { cursor = new Cursor(table.getDisplay(), SWT.CURSOR_HAND); } table.getShell().setCursor(cursor); } } private static class HyperlinksContentProvider extends StructuredContentProvider { @Override public Object[] getElements(final Object inputElement) { final List<?> hyperlinks = (List<?>) inputElement; return hyperlinks.toArray(); } } private static class HyperlinksLabelProvider extends RedCommonLabelProvider { @Override public StyledString getStyledText(final Object element) { final IHyperlink hyperlink = (IHyperlink) element; return new StyledString(hyperlink.getHyperlinkText()); } } }