/*- * Copyright © 2009 Diamond Light Source Ltd. * * This file is part of GDA. * * GDA is free software: you can redistribute it and/or modify it under the * terms of the GNU General Public License version 3 as published by the Free * Software Foundation. * * GDA is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along * with GDA. If not, see <http://www.gnu.org/licenses/>. */ package uk.ac.gda.richbeans.editors; import java.io.File; import java.lang.reflect.InvocationTargetException; import java.net.URL; import java.util.List; import org.eclipse.core.commands.operations.IUndoContext; import org.eclipse.core.commands.operations.OperationHistoryFactory; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.richbeans.api.binding.IBeanController; import org.eclipse.richbeans.api.binding.IBeanService; import org.eclipse.richbeans.api.event.ValueEvent; import org.eclipse.richbeans.api.event.ValueListener; import org.eclipse.richbeans.api.widget.IFieldProvider; import org.eclipse.richbeans.api.widget.IFieldWidget; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorSite; import org.eclipse.ui.IReusableEditor; import org.eclipse.ui.PartInitException; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.actions.WorkspaceModifyOperation; import org.eclipse.ui.part.EditorPart; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import uk.ac.gda.ServiceGrabber; import uk.ac.gda.common.rcp.util.EclipseUtils; import uk.ac.gda.util.beans.BeansFactory; import uk.ac.gda.util.beans.xml.XMLHelpers; /** * This class is designed to be extended and then the extending class edited with RCP developer. By naming the data entry fields the same as the fields in the * bean, this class will automatically save and open the bean values into the UI. * * @author Matthew Gerring */ public abstract class RichBeanEditorPart extends EditorPart implements ValueListener, IReusableEditor, IFieldProvider { protected static final Logger logger = LoggerFactory.getLogger(RichBeanEditorPart.class); /** * The bean used for state */ protected volatile Object editingBean; /** * A bean used in the undo stack assigned when the editor changes IEditorInput (normally at start up) */ protected volatile Object previousUndoBean; /** * An interface which provides for if the editor is dirty. */ protected final DirtyContainer dirtyContainer; /** * The file path which can be null */ protected String path; /** * The URL used in in saving the editing bean */ protected final URL mappingURL; private boolean undoStackActive = true; private boolean isDisposed = false; /** * Controller used to map ui <-> bean */ protected IBeanController controller; /** * Return the name that the editor should be referred as in error messages and in the multi-editor view. * * @return string */ protected abstract String getRichEditorTabText(); /** * @param path * @param mappingURL * @param dirtyContainer * @param editingBean */ public RichBeanEditorPart(final String path, final URL mappingURL, final DirtyContainer dirtyContainer, final Object editingBean) { this.path = path; this.mappingURL = mappingURL; this.dirtyContainer = dirtyContainer; this.editingBean = editingBean; if (getEditorUI() != null) { createDataBindingController(); } /** * The final undo state is recorded by cloning the editing bean and not editing it further when this editor is */ try { this.previousUndoBean = BeansFactory.deepClone(editingBean); } catch (Exception e) { try { logger.error("Cannot clone editing bean.", e); this.previousUndoBean = editingBean.getClass().newInstance(); } catch (Exception e1) { logger.error("Cannot instantiate editing bean.", e1); } } } protected void createDataBindingController() { IBeanService service = ServiceGrabber.getBeanService(); if (service == null) { throw new IllegalStateException("BeanService is not available"); } this.controller = service.createController(getEditorUI(), this.editingBean); } @Override public void doSave(IProgressMonitor monitor) { if (path == null) return; // Nothing to save. final File file = new File(path); monitor.beginTask(file.getName(), 100); try { try { updateFromUIAndReturnEditingBean(); WorkspaceModifyOperation saveOp = new WorkspaceModifyOperation() { @Override protected void execute(IProgressMonitor monitor) throws CoreException, InvocationTargetException, InterruptedException { try { XMLHelpers.writeToXML(mappingURL, editingBean, path); final IFile ifile = getIFile(); if (ifile != null) { ifile.refreshLocal(IResource.DEPTH_ZERO, null); } } catch (Exception e) { logger.error("Error - RichBeanEditorPart.doSave() failed. Path=" + path + e.getMessage()); MessageDialog dialog = new MessageDialog(getSite().getShell(), "File didn't save", null, "Path=" + path, MessageDialog.ERROR, new String[] {}, 0); int result = dialog.open(); System.out.println(result); throw new InvocationTargetException(e); } } }; PlatformUI.getWorkbench().getProgressService().busyCursorWhile(saveOp); notifyFileSaved(file); dirtyContainer.setDirty(false); } catch (Exception e) { // Saving is very important as it saves the state of the editors when switching between editors, perspectives, etc. logger.error("Error - RichBeanEditorPart.doSave() failed. Path=" + path + e.getMessage()); MessageDialog dialog = new MessageDialog(getSite().getShell(), "File didn't save", null, "Path=" + path, MessageDialog.ERROR, new String[] {}, 0); int result = dialog.open(); System.out.println(result); } } finally { monitor.done(); } } /** * Override to be called when a file is saved. * * @param file */ protected void notifyFileSaved(@SuppressWarnings("unused") File file) { } @Override public void doSaveAs() { // System.out.println("Do Save as Part"); } @Override public void init(IEditorSite site, IEditorInput input) throws PartInitException { setSite(site); setInput(input); if (dirtyContainer != null) dirtyContainer.setDirty(false); } /** * Might be null * * @return iFile */ public IFile getIFile() { return EclipseUtils.getIFile(getEditorInput()); } @Override public void setInput(IEditorInput input) { super.setInput(input); if (input != null) { setPartName(input.getName()); } // TODO this method should fire a property change as specified in the javadoc, but when this is implemented it will need testing } /** * @return the editingBean * @throws Exception */ public Object updateFromUIAndReturnEditingBean() throws Exception { controller.uiToBean(); return controller.getBean(); } /** * @return the path */ public String getPath() { return path; } /** * Should only be used internally. Do not override / change to be not final. * <p> * Be very careful calling this method, it risks losing synchronisation between the bean held in this editor and other references which should refer to the * same object (for example in a multi-page editor). * * @param editingBean * the editingBean to set */ protected final void setEditingBean(Object editingBean) { this.editingBean = editingBean; createDataBindingController(); } /** * @param path * the path to set */ public void setPath(String path) { this.path = path; } protected IUndoContext undoableContext; public void setUndoableContext(IUndoContext context) { this.undoableContext = context; } @Override public void valueChangePerformed(ValueEvent e) { dirtyContainer.setDirty(true); recordUndoableEvent(e.getFieldName()); } protected void recordUndoableEvent(final String fieldName) { if (!isUndoStackActive()) return; try { if (Thread.currentThread() != getSite().getShell().getDisplay().getThread()) return; // Save current bean state final Object previousBean = BeansFactory.deepClone(previousUndoBean); // Be careful about messing with this, very sensitive to make // this code correctly generic, try with many editors before // committing a change. Object tempNewBean = BeansFactory.deepClone(editingBean); updateOtherBeanFromUi(tempNewBean); Object newBean = BeansFactory.deepClone(tempNewBean); // If the values are the same as last edited, do nothing. if (previousUndoBean != null && previousUndoBean.equals(newBean)) return; // Add operation to stack. final RichBeanEditorOperation undoableOperation = new RichBeanEditorOperation(fieldName, previousBean, newBean, this); undoableOperation.addContext(undoableContext); OperationHistoryFactory.getOperationHistory().add(undoableOperation); previousUndoBean = newBean; getEditorSite().getActionBars().updateActionBars(); } catch (Exception e1) { logger.error("Unable to add event to stack " + editingBean.toString(), e1); } } private void updateOtherBeanFromUi(Object otherBean) throws Exception { IBeanService service = ServiceGrabber.getBeanService(); if (service == null) { throw new IllegalStateException("BeanService not available"); } service.createController(getEditorUI(), otherBean).uiToBean(); } @Override public String getValueListenerName() { return "DirtyListener"; } @Override public boolean isDirty() { if (path == null) return false; return dirtyContainer.isDirty(); } @Override public boolean isSaveAsAllowed() { return true; } protected boolean addedListenersAndSwitchedOn = false; /** * Extending classes should normally override this method with bounds and choice information and then call this method with super.linkUI(); */ public void linkUI(@SuppressWarnings("unused") final boolean isPageChange) { // Call a method which assigns properties from the scan parameters bean to // the ui in this class. This class can be used to do this for any // bean and any UI object (editor etc.) try { controller.switchState(false); controller.beanToUI(); controller.switchState(true); if (!addedListenersAndSwitchedOn) { controller.addValueListener(this); controller.recordBeanFields(); addedListenersAndSwitchedOn = true; // TODO resolve this - the DawnSci widgets do not allow expressions - a licensing issue?? // // We ensure that fields being edited which allow expressions, have the IExpressionManager // // available to evaluate the expressions for them. // BeanUI.notify(editingBean, getEditorUI(), new BeanProcessor() { // @Override // public void process(String name, Object value, IFieldWidget box) throws Exception { // if (box instanceof IExpressionWidget) { // final IExpressionWidget expressionBox = (IExpressionWidget)box; // if (expressionBox.isExpressionAllowed()){ // final BeanExpressionManager man = new BeanExpressionManager(expressionBox, RichBeanEditorPart.this); // man.setAllowedSymbols(getExpressionFields()); // expressionBox.setExpressionManager(man); // } // } // } // }); } } catch (Exception e) { logger.error("Cannot push values from bean to UI in linkUI()", e); } } /** * Override to define a different object for editing the UI. * * @return the editorUI object */ protected Object getEditorUI() { return this; } protected List<String> expressionFields; /** * Override this method (usually by calling it too) to add values which should be available in expressions. NOTE when overriding that after the first call * the expressionFields are cached to avoid too many interogations of the bean. * * @return List<String> of possible expression vars. * @throws Exception */ protected List<String> getExpressionFields() throws Exception { if (expressionFields == null) { expressionFields = controller.getEditingFields(); } return expressionFields; } @Override public IFieldWidget getField(final String fieldName) throws Exception { return controller.getFieldWidget(fieldName); } @Override public Object getFieldValue(final String fieldName) throws Exception { return getField(fieldName).getValue(); } @Override public void dispose() { setDisposed(true); super.dispose(); try { controller.dispose(); } catch (Exception e) { logger.error("Cannot dispose parts as expected", e); } } public boolean isDisposed() { return isDisposed; } public void setDisposed(boolean isDisposed) { this.isDisposed = isDisposed; } /** * @return Returns the undoStackActive. */ public boolean isUndoStackActive() { return undoStackActive; } /** * @param undoStackActive * The undoStackActive to set. */ public void setUndoStackActive(boolean undoStackActive) { this.undoStackActive = undoStackActive; } protected void uiToBean() throws Exception { controller.uiToBean(); } /** * This method will fire the UI's value listeners - be careful to avoid recursive update loops. For an alternative which does not fire the value listeners, * call {@link RichBeanEditorPart#linkUI(boolean isPageChange)} * * @throws Exception */ protected void beanToUI() throws Exception { controller.beanToUI(); } protected void switchState(boolean on) throws Exception { controller.switchState(on); } protected void addValueListener(ValueListener l) throws Exception { controller.addValueListener(l); } /** * Update the UI with values from the given bean. Intended to be used by, for example, undo/redo operations to update the field values without replacing the * editing bean and losing synchronisation with other editor pages. * <p> * This will fire the UI's value listeners so other clients are aware of the change. * * @param otherBean */ protected void updateUiFromOtherBean(Object otherBean) throws Exception { ServiceGrabber.getBeanService().createController(getEditorUI(), otherBean).beanToUI(); } }