/******************************************************************************* * Copyright (c) 2010 Philipp Kursawe. * 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: * Philipp Kursawe (phil.kursawe@gmail.com) - initial API and implementation ******************************************************************************/ package eclipseutils.jface.databinding; import java.lang.reflect.InvocationTargetException; import java.util.Collection; import java.util.Iterator; import java.util.List; import org.eclipse.core.databinding.DataBindingContext; import org.eclipse.core.databinding.beans.BeanProperties; import org.eclipse.core.databinding.observable.list.IObservableList; import org.eclipse.core.databinding.observable.list.WritableList; import org.eclipse.core.databinding.observable.value.ComputedValue; import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.core.databinding.property.Properties; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.SafeRunner; import org.eclipse.core.runtime.SubMonitor; import org.eclipse.jface.action.IMenuListener; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.databinding.swt.SWTObservables; import org.eclipse.jface.databinding.viewers.ObservableListContentProvider; import org.eclipse.jface.databinding.viewers.ObservableMapLabelProvider; import org.eclipse.jface.databinding.viewers.ViewersObservables; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.dialogs.ProgressMonitorDialog; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.layout.GridLayoutFactory; import org.eclipse.jface.layout.TableColumnLayout; import org.eclipse.jface.operation.IRunnableWithProgress; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.util.SafeRunnable; import org.eclipse.jface.viewers.CheckboxTableViewer; import org.eclipse.jface.viewers.ColumnWeightData; import org.eclipse.jface.viewers.DoubleClickEvent; import org.eclipse.jface.viewers.EditingSupport; import org.eclipse.jface.viewers.IBaseLabelProvider; import org.eclipse.jface.viewers.IDoubleClickListener; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.TableViewerColumn; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.KeyAdapter; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Table; import eclipseutils.jface.preferences.Messages; /** * * @author <a href="mailto:phil.kursawe@gmail.com">Philipp Kursawe</a> * */ public abstract class TableEditor<T> { /** Don't create add/remove buttons */ protected static final int READ_ONLY = 0x0001; /** Don't create the edit button. Used for Master/Detail view. */ protected static final int NO_EDIT_BUTTON = 0x0002; private TableViewer viewer; private Composite buttonBox; private IObservableList items; private DataBindingContext ctx; private final int flags; private IObservableValue viewerSingleSelectionValue; private Composite client; protected static interface Visitor<T> { void visit(T item, IProgressMonitor monitor); } protected TableEditor(final Composite parent, Collection<T> items, final int flags) { this.flags = flags; this.items = items != null ? (items instanceof IObservableList) ? (IObservableList) items : new WritableList(items, null) : new WritableList(); client = new Composite(parent, SWT.NULL); GridLayoutFactory.fillDefaults().numColumns(2).applyTo(client); createTableControl(client); GridDataFactory.fillDefaults().hint(200, 200).grab(true, true).applyTo( getViewer().getControl().getParent()); buttonBox = getButtonBoxControl(client); GridDataFactory.swtDefaults().align(SWT.FILL, SWT.BEGINNING).applyTo( buttonBox); } public Control getControl() { return client; } protected abstract String[] getColumnNames(); protected abstract String[] getColumnLabels(); /** * Creates a new item. Is called when the user clicks the add button. * * @param shell * to create a dialog on. * @return a newly created item, or <code>null</code> if a new item could * not be created. * @uithread This method is called from the UI-Thread. */ protected abstract T createItem(Shell shell); /** * <p> * Subclasses that do not use the Master/Details capabilities of this editor * must implement this and provide the user with the necessary means to edit * the given <i>item</i>. * * <p> * This standard implementation does nothing. * * @param shell * to create the editing user interface dialog on. * @param item * to edit */ protected void editItem(final Shell shell, final T item) { } protected int getTableStyles() { return SWT.BORDER | SWT.MULTI | SWT.V_SCROLL | SWT.H_SCROLL | SWT.FULL_SELECTION; } /** * @param parent * @return the table */ void createTableControl(final Composite parent) { final Composite client = new Composite(parent, SWT.NULL); // client.setLayout(new FillLayout(SWT.VERTICAL)); final TableColumnLayout tableLayout = new TableColumnLayout(); client.setLayout(tableLayout); int tableStyles = getTableStyles(); viewer = (tableStyles & SWT.CHECK) == SWT.CHECK ? CheckboxTableViewer .newCheckList(client, tableStyles) : new TableViewer(client, tableStyles); final Table table = viewer.getTable(); table.setLinesVisible(true); table.setHeaderVisible(true); table.setFont(parent.getFont()); table.addKeyListener(new KeyAdapter() { @Override public void keyPressed(final KeyEvent e) { if (e.keyCode == SWT.DEL) { removeSelected(); } } }); ctx = new DataBindingContext(); table.addDisposeListener(new DisposeListener() { public void widgetDisposed(final DisposeEvent e) { ctx.dispose(); viewer = null; } }); final String[] columnNames = getColumnNames(); final String[] columnLables = getColumnLabels(); for (int i = 0; i < columnNames.length; ++i) { final TableViewerColumn viewerColumn = new TableViewerColumn( viewer, SWT.LEFT); viewerColumn.getColumn().setText(columnLables[i]); viewerColumn.setEditingSupport(createEditingSupport(columnNames[i], viewer, ctx)); tableLayout .setColumnData(viewerColumn.getColumn(), new ColumnWeightData( i == columnNames.length - 1 ? 100 : 30)); } ObservableListContentProvider contentProvider = new ObservableListContentProvider(); viewer.setContentProvider(contentProvider); viewer.setLabelProvider(getLabelProvider(contentProvider)); viewer.setInput(items); getViewer().addDoubleClickListener(new IDoubleClickListener() { public void doubleClick(final DoubleClickEvent event) { editSelection(); } }); } protected IBaseLabelProvider getLabelProvider( ObservableListContentProvider contentProvider) { return new ObservableMapLabelProvider(Properties.observeEach( contentProvider.getKnownElements(), BeanProperties .values(getColumnNames()))); } /** * Subclasses may overwrite to add in-line editing support to the table. * * @param viewer * @param context * @param name * @return <code>null</code> in the default implementation. */ protected EditingSupport createEditingSupport(final String name, final TableViewer viewer, final DataBindingContext context) { return null; } public void setEnabled(final boolean enabled) { getViewer().getControl().setEnabled(enabled); for (final Control control : this.buttonBox.getChildren()) { control.setEnabled(enabled); } } /** * Returns the number of pixels corresponding to the given number of * horizontal dialog units. * <p> * Clients may call this framework method, but should not override it. * </p> * * @param control * the control being sized * @param dlus * the number of horizontal dialog units * @return the number of pixels */ protected int convertHorizontalDLUsToPixels(Control control, int dlus) { GC gc = new GC(control); gc.setFont(control.getFont()); int averageWidth = gc.getFontMetrics().getAverageCharWidth(); gc.dispose(); double horizontalDialogUnitSize = averageWidth * 0.25; return (int) Math.round(dlus * horizontalDialogUnitSize); } protected Button createPushButton(final Composite parent, final String text) { final Button button = new Button(parent, SWT.PUSH); button.setText(text); button.setFont(parent.getFont()); final GridData data = new GridData(GridData.FILL_HORIZONTAL); final int widthHint = convertHorizontalDLUsToPixels(button, IDialogConstants.BUTTON_WIDTH); data.widthHint = Math.max(widthHint, button.computeSize(SWT.DEFAULT, SWT.DEFAULT, true).x); button.setLayoutData(data); return button; } /** * Creates the add button. * * <p> * This standard implementation calls {@link #createItem(Shell)} when the * button is clicked and adds the returned item to the list. * * Subclasses may override to provide their own add button, or prevent the * creation of the button for read-only lists that can not have items added * or removed. * * @param parent * to create the button on */ protected Button createAddButton(final Composite parent) { if (isReadOnly()) { return null; } final Button button = createPushButton(parent, JFaceResources.getString("ListEditor.add")); //$NON-NLS-1$ createAddButtonMenu(button); button.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(final SelectionEvent event) { Menu menu = button.getMenu(); if (menu != null) { final Rectangle point = button.getBounds(); Point pos = button.toDisplay(point.x, point.height); button.getMenu().setLocation(pos.x, pos.y); button.getMenu().setVisible(true); } else { final T item = createItem(button.getShell()); if (item != null) { add(item, true); } } } }); return button; } protected void fillAddButtonMenu(IMenuManager menu) { } protected Button createRemoveButton(final Composite parent) { if (isReadOnly()) { return null; } final Button button = createPushButton(parent, JFaceResources.getString(JFaceResources.getString("ListEditor.remove"))); //$NON-NLS-1$ button.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(final SelectionEvent e) { if (MessageDialog.openQuestion(button.getShell(), Messages.AbstractTableViewerFieldEditor_Remove_Title, Messages.AbstractTableViewerFieldEditor_Remove_Message)) { removeSelected(); } } }); enableWithSelection(button, SWT.MULTI | SWT.SINGLE); return null; } protected Button createEditButton(final Composite parent) { final Button button = createPushButton(parent, Messages.AbstractTableViewerFieldEditor_Edit_Label); button.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(final SelectionEvent e) { editSelection(); } }); enableWithSelection(button, SWT.SINGLE); return button; } @SuppressWarnings("unchecked") private void editSelection() { if ((flags & NO_EDIT_BUTTON) == NO_EDIT_BUTTON) { return; } final T item = (T) getViewerSelectionValue().getValue(); if (item != null) { editItem(this.getViewer().getControl().getShell(), item); } } private void createButtons(final Composite parent) { createAddButton(parent); createEditButton(parent); createRemoveButton(parent); createCustomButtons(parent); } public TableViewer getViewer() { return viewer; } protected IObservableValue getViewerSelectionValue() { if (viewerSingleSelectionValue == null) { viewerSingleSelectionValue = ViewersObservables .observeSingleSelection(getViewer()); } return viewerSingleSelectionValue; } protected void visitViewerSelection(final String operationName, final Visitor<T> visitor) { final IStructuredSelection viewerSelection = (IStructuredSelection) viewer .getSelection(); final IRunnableWithProgress runnable = new IRunnableWithProgress() { @SuppressWarnings("unchecked") public void run(final IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { try { monitor.beginTask(operationName, viewerSelection.size()); final SubMonitor progress = SubMonitor .convert(monitor, 100); final Iterator<?> it = viewerSelection.iterator(); while (it.hasNext()) { if (progress.isCanceled()) { throw new InterruptedException(); } visitor.visit((T) it.next(), progress.newChild(1)); } } finally { monitor.done(); } } }; SafeRunner.run(new SafeRunnable(operationName) { public void run() throws Exception { final ProgressMonitorDialog dialog = new ProgressMonitorDialog( getViewer().getControl().getShell()); dialog.run(true, true, runnable); } }); } protected void enableWithSelection(final Control control, final int type) { bindToViewerSelection(SWTObservables.observeEnabled(control), type); } protected void bindToViewerSelection(final IObservableValue target, final int type) { Assert.isLegal((type & SWT.MULTI | SWT.SINGLE) != 0); if (ctx != null) { IObservableValue selected = new ComputedValue(Boolean.TYPE) { protected Object calculate() { return Boolean .valueOf(getViewerSelectionValue().getValue() != null); } }; ctx.bindValue(target, selected); } } /** * Allows subclasses to add custom buttons after the standard * add/edit/remove group of buttons. * * <p> * The default implementation does nothing. * * @param parent * to create the buttons on */ protected void createCustomButtons(final Composite parent) { } protected Composite getButtonBoxControl(final Composite parent) { if (buttonBox == null) { buttonBox = new Composite(parent, SWT.NULL); GridLayoutFactory.fillDefaults().applyTo(buttonBox); createButtons(buttonBox); buttonBox.addDisposeListener(new DisposeListener() { public void widgetDisposed(final DisposeEvent event) { buttonBox = null; } }); } // selectionChanged(); return buttonBox; } @SuppressWarnings("unchecked") public List<T> getItems() { return items; } public void clear() { items.getRealm().exec(new Runnable() { public void run() { items.clear(); } }); } public void add(final T item, final boolean select) { items.getRealm().exec(new Runnable() { public void run() { items.add(item); if (select) { viewer.setSelection(new StructuredSelection(item)); } } }); } public void remove(final T item) { items.getRealm().exec(new Runnable() { public void run() { items.remove(item); } }); } public void addAll(final Collection<T> adds) { items.getRealm().exec(new Runnable() { public void run() { items.addAll(adds); } }); } protected boolean isReadOnly() { return ((flags & READ_ONLY) == READ_ONLY); } private void removeSelected() { visitViewerSelection( Messages.AbstractTableViewerFieldEditor_Remove_JobName, new Visitor<T>() { public void visit(final T item, final IProgressMonitor monitor) { remove(item); } }); } protected void createAddButtonMenu(Control button) { MenuManager menu = new MenuManager("#PopupMenu"); menu.setRemoveAllWhenShown(true); menu.addMenuListener(new IMenuListener() { public void menuAboutToShow(IMenuManager manager) { fillAddButtonMenu(manager); } }); button.setMenu(menu.createContextMenu(button)); } }