/******************************************************************************* * Copyright (c) 2007, 2014 compeople AG and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * compeople AG - initial API and implementation *******************************************************************************/ package org.eclipse.riena.internal.ui.ridgets.swt; import java.beans.PropertyDescriptor; import java.util.Collection; import java.util.Comparator; import java.util.Map; import javax.swing.ToolTipManager; import org.osgi.service.log.LogService; import org.eclipse.core.databinding.beans.PojoObservables; import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.core.runtime.Assert; import org.eclipse.equinox.log.Logger; import org.eclipse.jface.viewers.AbstractTableViewer; import org.eclipse.jface.viewers.CheckboxTableViewer; import org.eclipse.jface.viewers.IBaseLabelProvider; import org.eclipse.jface.viewers.ICheckStateProvider; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.TableViewerColumn; import org.eclipse.jface.viewers.ViewerColumn; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.ControlListener; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseMoveListener; import org.eclipse.swt.events.MouseTrackAdapter; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Item; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.TableItem; import org.eclipse.swt.widgets.Widget; import org.eclipse.riena.core.Log4r; import org.eclipse.riena.core.util.RAPDetector; import org.eclipse.riena.core.util.StringUtils; import org.eclipse.riena.ui.core.marker.RowErrorMessageMarker; import org.eclipse.riena.ui.ridgets.IColumnFormatter; import org.eclipse.riena.ui.ridgets.listener.ClickEvent; import org.eclipse.riena.ui.ridgets.swt.MarkerSupport; import org.eclipse.riena.ui.ridgets.swt.SortableComparator; import org.eclipse.riena.ui.swt.facades.SWTFacade; import org.eclipse.riena.ui.swt.facades.TableRidgetToolTipSupportFacade; import org.eclipse.riena.ui.swt.facades.internal.ITableRidgetToolTipSupport; import org.eclipse.riena.ui.swt.lnf.LnfKeyConstants; import org.eclipse.riena.ui.swt.lnf.LnfManager; import org.eclipse.riena.ui.swt.utils.SwtUtilities; /** * Ridget for SWT {@link Table} widgets. */ public class TableRidget extends AbstractTableRidget { private final static Logger LOGGER = Log4r.getLogger(TableRidget.class); private TableTooltipManager tooltipManager; private ITableRidgetToolTipSupport tooltipSupport; private ControlListener columnResizeListener; private final Listener itemEraser; public TableRidget() { super(); sortListener = new ColumnSortListener(); itemEraser = new TableItemEraser(); } /** * {@inheritDoc} * <p> * The given {@code uiControl} must be a {@link Table}. */ @Override protected final void checkUIControl(final Object uiControl) { checkType(uiControl, Table.class); } @Override protected final void bindUIControl() { final Table control = getUIControl(); if (control != null) { control.removeAll(); } super.bindUIControl(); if (control != null) { updateColumnAlignment(); columnResizeListener = new ControlListener() { public void controlResized(final ControlEvent e) { applyTableColumnHeaders(); } public void controlMoved(final ControlEvent e) { applyTableColumnHeaders(); } }; for (final TableColumn column : control.getColumns()) { column.addSelectionListener(sortListener); column.addControlListener(columnResizeListener); } control.addSelectionListener(selectionTypeEnforcer); final SWTFacade facade = SWTFacade.getDefault(); facade.addEraseItemListener(control, itemEraser); // when using TableLayout, tables do not consider the presence of a vertical scroll bar and the space it needs // in case that the table is created with a vertical scroll bar, also a horizontal bar will appear (swt/jface problem) // this listener is a workaround for this behavior to trigger a layout ONCE and disables itself control.addListener(SWTFacade.Paint, new Listener() { private boolean disable; public void handleEvent(final Event event) { if (disable) { return; } disable = true; control.getParent().layout(true, true); } }); } } @Override protected final void unbindUIControl() { super.unbindUIControl(); final Table control = getUIControl(); if (control != null) { for (final TableColumn column : control.getColumns()) { column.removeSelectionListener(sortListener); column.removeControlListener(columnResizeListener); } control.removeSelectionListener(selectionTypeEnforcer); final SWTFacade facade = SWTFacade.getDefault(); facade.removeEraseItemListener(control, itemEraser); if (tooltipManager != null) { facade.removeMouseTrackListener(control, tooltipManager); facade.removeMouseMoveListener(control, tooltipManager); } if (tooltipSupport != null) { tooltipSupport.disableSupport(); } } } @Override protected final int getUiSelectionCount() { final Table control = getUIControl(); return control == null ? -1 : control.getSelectionCount(); } // @Override // protected final int getUiSelectionIndex() { // final Table control = getUIControl(); // return control == null ? -1 : control.getSelectionIndex(); // } // // @Override // protected final void setUiSelection(final int index) { // final Table control = getUIControl(); // if (control == null) { // return; // } // control.setSelection(index); // } @Override protected final void setUiSelection(final Widget item) { Assert.isTrue(item instanceof TableItem); final Table control = getUIControl(); if (control != null) { control.setSelection((TableItem) item); } } @Override public int getSelectionIndex() { final Table control = getUIControl(); return control == null ? -1 : control.getSelectionIndex(); } @Override public int[] getSelectionIndices() { final Table control = getUIControl(); return control == null ? new int[0] : control.getSelectionIndices(); } @Override public Table getUIControl() { return (Table) super.getUIControl(); } @Override protected TableViewer getTableViewer() { return (TableViewer) super.getTableViewer(); } @Override protected final void applyColumns() { final Table control = getUIControl(); if (control == null) { return; } final int expectedCols = getExpectedColumnCount(); if (getColumnCount() != expectedCols) { for (final TableColumn column : control.getColumns()) { column.dispose(); } for (int i = 0; i < expectedCols; i++) { new TableColumn(control, SWT.NONE); } applyColumnWidths(); } final TableColumn[] columns = control.getColumns(); for (int columnIndex = 0; columnIndex < columns.length; columnIndex++) { final ViewerColumn viewerColumn = new TableViewerColumn(getTableViewer(), columns[columnIndex]); applyEditingSupport(viewerColumn, columnIndex); } } @Override protected final void applyColumnsMovable() { final Table control = getUIControl(); if (control == null) { return; } for (final TableColumn column : control.getColumns()) { column.setMoveable(hasMoveableColumns()); } } @Override protected final void applyComparator(final Map<Integer, Comparator<?>> comparatorMap) { if (getTableViewer() != null) { Comparator<?> compi = null; if (getSortedColumn() != -1) { final Integer key = Integer.valueOf(getSortedColumn()); compi = comparatorMap.get(key); } final Table table = getUIControl(); if (compi != null) { final TableColumn column = table.getColumn(getSortedColumn()); table.setSortColumn(column); final int direction = getSortDirection(); table.setSortDirection(direction); final SortableComparator sortableComparator = new SortableComparator(this, compi); getTableViewer().setComparator(new TableComparator(sortableComparator)); } else { getTableViewer().setComparator(null); table.setSortColumn(null); table.setSortDirection(SWT.NONE); } } } @Override protected final void applyTableColumnHeaders() { final Table control = getUIControl(); if (control == null) { return; } final boolean headersVisible = columnHeaders != null; control.setHeaderVisible(headersVisible); if (headersVisible) { final TableColumn[] columns = control.getColumns(); for (int i = 0; i < columns.length; i++) { String columnHeader = ""; //$NON-NLS-1$ if (i < columnHeaders.length && columnHeaders[i] != null) { columnHeader = columnHeaders[i]; } columns[i].setText(columnHeader); final String tooltip = isShowColumnTooltip(columns[i], columnHeader) ? columnHeader : ""; //$NON-NLS-1$ columns[i].setToolTipText(tooltip); } } } @Override protected int getColumnStyle(final int columnIndex) { checkColumnRange(columnIndex); final Table control = getUIControl(); if (control == null) { return SWT.DEFAULT; } final TableColumn[] columns = control.getColumns(); return columns[columnIndex].getStyle(); } /** * {@inheritDoc} * <p> * This TableRidget provides two different tool tip supports. * <ul> * <li>native: The inner class {@link ToolTipManager} shows appropriate tool tips (cell item text, error marker text, etc.) that looks like native tool * tips.</li> * <li>JFace: The class {@link TableRidgetToolTipSupportFacade} shows also appropriate tool tips. But the look of the tool tips can be configured (see * {@link IColumnFormatter}). Also images can be displayed.</li> * </ul> */ @Override protected final void updateToolTipSupport() { if (getUIControl() == null) { return; } final SWTFacade facade = SWTFacade.getDefault(); if (isNativeToolTip() || !TableRidgetToolTipSupportFacade.getDefault().isSupported()) { if (tooltipSupport != null) { tooltipSupport.disableSupport(); } if (tooltipManager == null) { tooltipManager = new TableTooltipManager(); tooltipManager.init(getUIControl()); } facade.addMouseTrackListener(getUIControl(), tooltipManager); facade.addMouseMoveListener(getUIControl(), tooltipManager); } else { if (tooltipManager != null) { facade.removeMouseTrackListener(getUIControl(), tooltipManager); facade.removeMouseMoveListener(getUIControl(), tooltipManager); } if (getTableViewer() instanceof TableRidgetTableViewer) { if (tooltipSupport == null) { tooltipSupport = TableRidgetToolTipSupportFacade.getDefault().enableFor(getTableViewer()); } else { tooltipSupport.enableSupport(getTableViewer()); } } } } /** * Updates the aligmnent of every Column. The Aligmnent is taken from the ColumnFormatters attached to this Ridget. */ private void updateColumnAlignment() { if (null != getUIControl()) { final TableColumn[] tableColumns = getUIControl().getColumns(); final IColumnFormatter[] columnFormatters = getColumnFormatters(getUIControl().getColumnCount()); for (int i = 0; i < tableColumns.length; i++) { if (columnFormatters.length > i && columnFormatters[i] != null) { if (columnFormatters[i].getHorizontalAlignment(null) == SWT.DEFAULT) { tableColumns[i].setAlignment(tableColumns[i].getAlignment()); } else { tableColumns[i].setAlignment(columnFormatters[i].getHorizontalAlignment(null)); } } } } } @Override public void setColumnFormatter(final int columnIndex, final IColumnFormatter formatter) { super.setColumnFormatter(columnIndex, formatter); updateColumnAlignment(); } @Override protected void configureViewer(final AbstractTableViewer viewer) { /** * As the viewer can be shared between multiple ridgets, super.configureViewer will partly work on a dirty viewer state till the whole new model is * transferred to the viewer. (labelprovider, attributeMap, contentProvider, ..). In the meantime the viewer should not be refreshed to prevent errors * as the consequence of this inconsistency. Therefore we set a flag on the viewer signaling to ignore refresh events. After the update of the viewer * the flag is erased. */ if (viewer instanceof TableRidgetTableViewer) { ((TableRidgetTableViewer) viewer).setAllowRefresh(false); } super.configureViewer(viewer); if (viewer instanceof TableRidgetTableViewer) { ((TableRidgetTableViewer) viewer).setAllowRefresh(true); } if (isCheckBoxInFirstColumn(viewer)) { ((CheckboxTableViewer) viewer).setCheckStateProvider(new TableRidgetCheckStateProvider()); } viewer.refresh(); } @Override protected void configureLableProvider(final IBaseLabelProvider labelProvider, final AbstractTableViewer viewer) { super.configureLableProvider(labelProvider, viewer); if (labelProvider instanceof TableRidgetLabelProvider) { ((TableRidgetLabelProvider) labelProvider).setCheckBoxInFirstColumn(isCheckBoxInFirstColumn(viewer)); } } public boolean isCheckBoxInFirstColumn(final AbstractTableViewer viewer) { if (SwtUtilities.isDisposed(viewer.getControl())) { return false; } if (!(viewer.getControl() instanceof Table)) { return false; } if ((viewer.getControl().getStyle() & SWT.CHECK) != SWT.CHECK) { return false; } final Table table = (Table) viewer.getControl(); if (table.getColumnCount() <= 0) { LOGGER.log(LogService.LOG_WARNING, "SWT table with style SWT.CHECK but no columns (column count <= 0)"); //$NON-NLS-1$ return false; } if (!(viewer instanceof CheckboxTableViewer)) { LOGGER.log(LogService.LOG_WARNING, "SWT table with style SWT.CHECK but with wrong viewer (not CheckboxTableViewer)"); //$NON-NLS-1$ return false; } final PropertyDescriptor property = getPropertyDescriptor(renderingMethods[0]); if (property.getPropertyType() != boolean.class) { LOGGER.log(LogService.LOG_WARNING, "SWT table with style SWT.CHECK but wrong property type at first column (not boolean)"); //$NON-NLS-1$ return false; } return true; } @Override protected AbstractTableViewer createTableViewer() { return new TableRidgetTableViewer(this); } @Override protected TableWrapper createTableWrapper() { Assert.isNotNull(getUIControl()); return new TableWrapper(getUIControl()); } @Override protected ClickEvent createClickEvent(final MouseEvent e) { final Table table = (Table) e.widget; final int colIndex = SwtUtilities.findColumn(table, new Point(e.x, e.y)); // x = 0 gets us an item even not using SWT.FULL_SELECTION final Item item = getItem(new Point(0, e.y)); final Object rowData = item != null ? item.getData() : null; final ClickEvent event = new ClickEvent(this, e.button, colIndex, rowData); return event; } /** * Returns the width of the table column in pixel. * * @param control * @param str * @return */ private int columnTextWidth(final TableColumn control, final String str) { final GC g = new GC(control.getParent()); final Font of = g.getFont(); g.setFont(control.getParent().getFont()); final Point extent = g.stringExtent(str); g.setFont(of); g.dispose(); // TODO check if offset in table column differs on various platforms return extent.x + 16; } private boolean isShowColumnTooltip(final TableColumn col, final String columnText) { if (RAPDetector.isRAPavailable()) { return false; } return col.getWidth() < columnTextWidth(col, columnText); } // helping classes // //////////////// /** * This class provides if the check box in the first column of the table is check or not. */ private final class TableRidgetCheckStateProvider implements ICheckStateProvider { public boolean isGrayed(final Object element) { return false; } public boolean isChecked(final Object element) { if ((renderingMethods == null) || (renderingMethods.length <= 0)) { LOGGER.log(LogService.LOG_WARNING, "No property found for first column!"); //$NON-NLS-1$ return false; } final String propertyName = renderingMethods[0]; final IObservableValue observableValue = PojoObservables.observeValue(element, propertyName); final Object value = observableValue.getValue(); if (value instanceof Boolean) { return (Boolean) value; } else { LOGGER.log(LogService.LOG_WARNING, "Unexpected property type of first column!"); //$NON-NLS-1$ } return false; } } /** * Selection listener for table headers that changes the sort order of a column according to the information stored in the ridget. */ private final class ColumnSortListener extends SelectionAdapter { @Override public void widgetSelected(final SelectionEvent e) { final TableColumn column = (TableColumn) e.widget; final int columnIndex = column.getParent().indexOf(column); final int direction = column.getParent().getSortDirection(); if (columnIndex == getSortedColumn()) { if (direction == SWT.UP) { setSortedAscending(false); } else if (direction == SWT.DOWN) { setSortedColumn(-1); } } else if (isColumnSortable(columnIndex)) { setSortedColumn(columnIndex); if (direction == SWT.NONE) { setSortedAscending(true); } } column.getParent().showSelection(); } } /** * Shows the appropriate tooltip (error tooltip / regular tooltip / no tooltip) for the current hovered row. */ private final class TableTooltipManager extends MouseTrackAdapter implements MouseMoveListener { private String defaultToolTip; public void init(final Control table) { final String tableToolTip = table.getToolTipText(); defaultToolTip = tableToolTip != null ? tableToolTip : ""; //$NON-NLS-1$ } public void mouseMove(final MouseEvent event) { final Control table = (Control) event.widget; hideToolTip(table); } @Override public void mouseExit(final MouseEvent event) { final Control table = (Control) event.widget; resetToolTip(table); } @Override public void mouseHover(final MouseEvent event) { String errorToolTip = ""; //$NON-NLS-1$ String itemToolTip = ""; //$NON-NLS-1$ final Point mousePt = new Point(event.x, event.y); final TableItem item = getItem(mousePt); if (item != null) { errorToolTip = getErrorToolTip(item); itemToolTip = getItemToolTip(item, mousePt); } final Control table = (Control) event.widget; if (!StringUtils.isEmpty(errorToolTip)) { table.setToolTipText(errorToolTip); } else if (!StringUtils.isEmpty(itemToolTip)) { table.setToolTipText(itemToolTip); } else { resetToolTip(table); } } // helping methods ////////////////// private TableItem getItem(final Point point) { final Table control = getUIControl(); if (control == null) { return null; } return control.getItem(point); } private String getItemToolTip(final TableItem item, final Point mousePt) { String result = null; final int column = SwtUtilities.findColumn(item.getParent(), mousePt); if (column != -1) { final IBaseLabelProvider labelProvider = getTableViewer().getLabelProvider(); if (labelProvider != null) { final Object element = item.getData(); result = ((TableRidgetLabelProvider) labelProvider).getToolTipText(element, column); } if (result == null) { result = item.getText(column); } } return result; } private String getErrorToolTip(final Item item) { if (item != null) { final Object data = item.getData(); final Collection<RowErrorMessageMarker> markers = getMarkersOfType(RowErrorMessageMarker.class); for (final RowErrorMessageMarker marker : markers) { if (marker.getRowValue() == data) { return marker.getMessage(); } } } return null; } private void hideToolTip(final Control table) { if (!"".equals(table.getToolTipText())) { //$NON-NLS-1$ table.setToolTipText(""); //$NON-NLS-1$ } } private void resetToolTip(final Control table) { if (table.getToolTipText() == null || !table.getToolTipText().equals(defaultToolTip)) { table.setToolTipText(defaultToolTip); } } } /** * Erase listener for custom painting of table cells. It is responsible for:[ * <ul> * <li>erasing (emptying) all cells when this ridget is disabled and {@link LnfKeyConstants#DISABLED_MARKER_HIDE_CONTENT} is true</li> * <li>drawing a red border around cells that have been marked with a {@link RowErrorMessageMarker} (unless disabled)</li> * </ul> * * @see '<a href= "http://www.eclipse.org/articles/article.php?file=Article-CustomDrawingTableAndTreeItems/index.html" >Custom Drawing Table and Tree * Items</a>' */ private final class TableItemEraser implements Listener { private final Color borderColor; private final int borderThickness; public TableItemEraser() { borderColor = LnfManager.getLnf().getColor(LnfKeyConstants.ERROR_MARKER_BORDER_COLOR); borderThickness = LnfManager.getLnf().getIntegerSetting(LnfKeyConstants.ROW_ERROR_MARKER_BORDER_THICKNESS, 1); } /* * Called EXTREMELY frequently. Must be as efficient as possible. */ public void handleEvent(final Event event) { if (isHidingWhenDisabled()) { hideContent(event); } else { if (isErrorMarked(event.item)) { markRow(event); } } } // helping methods ////////////////// private void hideContent(final Event event) { // we indicate custom fg drawing, but don't draw foreground => hide event.detail &= ~SWT.FOREGROUND; } private boolean isHidingWhenDisabled() { return !isEnabled() && MarkerSupport.isHideDisabledRidgetContent(); } private void markRow(final Event event) { final GC gc = event.gc; final Color oldForeground = gc.getForeground(); gc.setForeground(borderColor); try { int x = 0, y = 0, width = 0, height = 0; final int colCount = getColumnCount(); if (colCount > 0) { final TableItem item = (TableItem) event.item; for (int i = 0; i < colCount; i++) { final Rectangle bounds = item.getBounds(i); if (i == 0) { // start 3px to the left of first column x = bounds.x - 3; y = bounds.y; width += 3; } width += bounds.width; height = Math.max(height, bounds.height); } width = Math.max(0, width - 1); height = Math.max(0, height - 1); } else { width = Math.max(0, event.width - 1); height = Math.max(0, event.height - 1); x = event.x; y = event.y; } for (int i = 0; i < borderThickness; i++) { int arc = 3; if (i > 0) { arc = 0; } gc.drawRoundRectangle(x + i, y + i, width - 2 * i, height - 2 * i, arc, arc); } } finally { gc.setForeground(oldForeground); } } } }