package info.limpet.stackedcharts.ui.view; import info.limpet.stackedcharts.model.ChartSet; import info.limpet.stackedcharts.model.IndependentAxis; import info.limpet.stackedcharts.model.StackedchartsFactory; import info.limpet.stackedcharts.ui.editor.Activator; import info.limpet.stackedcharts.ui.editor.StackedchartsEditControl; import info.limpet.stackedcharts.ui.view.adapter.IStackedTimeListener; import info.limpet.stackedcharts.ui.view.adapter.IStackedTimeProvider; import java.awt.Color; import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; import java.awt.geom.Rectangle2D; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IConfigurationElement; import org.eclipse.core.runtime.IExtension; import org.eclipse.core.runtime.IExtensionPoint; import org.eclipse.core.runtime.IExtensionRegistry; import org.eclipse.core.runtime.Platform; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl; import org.eclipse.gef.commands.CommandStack; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IAction; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.IToolBarManager; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.ISelectionProvider; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.nebula.effects.stw.Transition; import org.eclipse.nebula.effects.stw.TransitionListener; import org.eclipse.nebula.effects.stw.TransitionManager; import org.eclipse.nebula.effects.stw.Transitionable; import org.eclipse.nebula.effects.stw.transitions.CubicRotationTransition; import org.eclipse.swt.SWT; import org.eclipse.swt.dnd.DND; import org.eclipse.swt.dnd.DropTarget; import org.eclipse.swt.dnd.DropTargetEvent; import org.eclipse.swt.dnd.DropTargetListener; import org.eclipse.swt.dnd.FileTransfer; import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseListener; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.IActionBars; import org.eclipse.ui.IViewSite; import org.eclipse.ui.part.ViewPart; import org.eclipse.ui.plugin.AbstractUIPlugin; import org.eclipse.ui.views.properties.IPropertySheetPage; import org.eclipse.ui.views.properties.tabbed.ITabbedPropertySheetPageContributor; import org.jfree.chart.JFreeChart; import org.jfree.chart.StandardChartTheme; import org.jfree.experimental.chart.swt.ChartComposite; public class StackedChartsView extends ViewPart implements ITabbedPropertySheetPageContributor, ISelectionProvider, DisposeListener, IStackedTimeListener { /** * interface for external objects that are able to supply a date and resond to a new date * * @author ian * */ public static interface ControllableDate { /** * retrieve the date * * @return current date */ Date getDate(); /** * control the date * * @param time */ void setDate(Date date); } private static final int MARKER_STEP_SIZE = 1; public static final String STACKED_CHARTS_CONFIG = "StackedCharts"; public static final int CHART_VIEW = 1; public static final int EDIT_VIEW = 2; public static final String ID = "info.limpet.StackedChartsView"; private static final String TIME_PROVIDER_ID = "stacked_time_provider"; private StackedPane stackedPane; // effects protected TransitionManager transitionManager = null; private Composite chartHolder; private Composite editorHolder; private ChartSet charts; private final AtomicBoolean initEditor = new AtomicBoolean(true); private StackedchartsEditControl chartEditor; private final List<ISelectionChangedListener> selectionListeners = new ArrayList<ISelectionChangedListener>(); private Date _currentTime; private ChartComposite _chartComposite; private ArrayList<Runnable> _closeCallbacks; private ControllableDate _controllableDate = null; private JFreeChart jFreeChart; /** * flag for if we're currently in update * */ private static boolean _amUpdating = false; /** * value we use for null-time * */ private final long INVALID_TIME = -1L; /** * we don't want to process all new-time events, only the most recent one. So, take a note of the * most recent one */ AtomicLong _pendingTime = new AtomicLong(INVALID_TIME); /** * let classes pass callbacks to be run when we are closing * * @param runnable */ public void addRunOnCloseCallback(final Runnable runnable) { if (_closeCallbacks == null) { _closeCallbacks = new ArrayList<Runnable>(); } _closeCallbacks.add(runnable); } @Override public void addSelectionChangedListener( final ISelectionChangedListener listener) { selectionListeners.add(listener); } /** * convenience method to make the value marker labels larger or smaller * * @param up */ private void changeFontSize(final Boolean up) { // are we making a change? final float change; if (up != null) { // sort out which direction we're changing if (up) { change = MARKER_STEP_SIZE; } else { change = -MARKER_STEP_SIZE; } } else { change = 0; } // do we have a default? final float curSize = Activator.getDefault().getPreferenceStore().getFloat( TimeBarPlot.CHART_FONT_SIZE_NODE); // trim to reasonable size final float sizeToUse = Math.max(9, curSize); // produce new size final float newVal = sizeToUse + change; // store the new size Activator.getDefault().getPreferenceStore().setValue( TimeBarPlot.CHART_FONT_SIZE_NODE, newVal); final float base = newVal; final StandardChartTheme theme = (StandardChartTheme) StandardChartTheme.createJFreeTheme(); theme.setRegularFont(theme.getRegularFont().deriveFont(base * 1.0f)); theme.setExtraLargeFont(theme.getExtraLargeFont().deriveFont(base * 1.4f)); theme.setLargeFont(theme.getLargeFont().deriveFont(base * 1.2f)); theme.setSmallFont(theme.getSmallFont().deriveFont(base * 0.8f)); theme.setChartBackgroundPaint(Color.white); theme.setPlotBackgroundPaint(Color.white); theme.setGridBandPaint(Color.lightGray); theme.setDomainGridlinePaint(Color.lightGray); theme.setRangeGridlinePaint(Color.lightGray); theme.apply(jFreeChart); } protected void connectFileDropSupport(final Control compoent) { final DropTarget target = new DropTarget(compoent, DND.DROP_MOVE | DND.DROP_COPY | DND.DROP_DEFAULT); final FileTransfer fileTransfer = FileTransfer.getInstance(); target.setTransfer(new Transfer[] {fileTransfer}); target.addDropListener(new DropTargetListener() { @Override public void dragEnter(final DropTargetEvent event) { if (event.detail == DND.DROP_DEFAULT) { if ((event.operations & DND.DROP_COPY) != 0) { event.detail = DND.DROP_COPY; } else { event.detail = DND.DROP_NONE; } } for (int i = 0; i < event.dataTypes.length; i++) { if (fileTransfer.isSupportedType(event.dataTypes[i])) { event.currentDataType = event.dataTypes[i]; // files should only be copied if (event.detail != DND.DROP_COPY) { event.detail = DND.DROP_NONE; } break; } } } @Override public void dragLeave(final DropTargetEvent event) { } @Override public void dragOperationChanged(final DropTargetEvent event) { if (event.detail == DND.DROP_DEFAULT) { if ((event.operations & DND.DROP_COPY) != 0) { event.detail = DND.DROP_COPY; } else { event.detail = DND.DROP_NONE; } } if (fileTransfer.isSupportedType(event.currentDataType)) { if (event.detail != DND.DROP_COPY) { event.detail = DND.DROP_NONE; } } } @Override public void dragOver(final DropTargetEvent event) { } @Override public void drop(final DropTargetEvent event) { if (fileTransfer.isSupportedType(event.currentDataType)) { final String[] files = (String[]) event.data; // *.stackedcharts if (files.length == 1 && files[0].endsWith("stackedcharts")) { final File file = new File(files[0]); final Resource resource = new ResourceSetImpl().createResource(URI.createURI(file.toURI() .toString())); try { resource.load(new HashMap<>()); final ChartSet chartsSet = (ChartSet) resource.getContents().get(0); setModel(chartsSet); } catch (final IOException e) { e.printStackTrace(); MessageDialog.openError(Display.getCurrent().getActiveShell(), "Error", e.getMessage()); } } } } @Override public void dropAccept(final DropTargetEvent event) { } }); } protected void contributeToActionBars() { final IActionBars bars = getViewSite().getActionBars(); fillLocalPullDown(bars.getMenuManager()); fillLocalToolBar(bars.getToolBarManager()); } protected Control createChartView() { // defer creation of the actual chart until we receive // some model data. So, just have an empty panel // to start with chartHolder = new Composite(stackedPane, SWT.NONE); chartHolder.setLayout(new FillLayout()); return chartHolder; } protected Control createEditView() { editorHolder = new Composite(stackedPane, SWT.NONE); editorHolder.setLayout(new FillLayout()); // create gef base editor chartEditor = new StackedchartsEditControl(editorHolder); // proxy editor selection to view site chartEditor.getViewer().addSelectionChangedListener( new ISelectionChangedListener() { @Override public void selectionChanged(final SelectionChangedEvent event) { fireSelectionChnaged(); } }); return editorHolder; } @Override public void createPartControl(final Composite parent) { getViewSite().setSelectionProvider(this);// setup proxy selection provider stackedPane = new StackedPane(parent); // note: The "Show in ..." action specifies a unique secondary id, // which it uses to force a new instance. Hence, if a secondary // id isn't provided we presume a blank chart is being requested String secondaryId = ((IViewSite) getSite()).getSecondaryId(); if (secondaryId != null) { stackedPane.add(CHART_VIEW, createChartView()); stackedPane.add(EDIT_VIEW, createEditView()); selectView(CHART_VIEW); } else { // blank view // order is different stackedPane.add(EDIT_VIEW, createEditView()); stackedPane.add(CHART_VIEW, createChartView()); ChartSet blankModel = createBlankModel(); setModel(blankModel, EDIT_VIEW); } contributeToActionBars(); chartEditor.init(this); // Drop Support for *.stackedcharts connectFileDropSupport(stackedPane); final boolean IS_LINUX_OS = System.getProperty("os.name").toLowerCase().indexOf("nux") >= 0; final Image[] compImage = new Image[2]; // stackedPane comp count parent.addDisposeListener(new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { for (Image img : compImage) { if (img != null) { img.dispose(); } } } }); final Transitionable transitionable; transitionManager = new TransitionManager(transitionable = new Transitionable() { @Override public void addSelectionListener(final SelectionListener listener) { stackedPane.addSelectionListener(listener); } @Override public Composite getComposite() { return stackedPane; } @Override public Control getControl(final int index) { return stackedPane.getControl(index); } @Override public double getDirection(final int toIndex, final int fromIndex) { return toIndex == CHART_VIEW ? Transition.DIR_RIGHT : Transition.DIR_LEFT; } @Override public int getSelection() { return stackedPane.getActiveControlKey(); } @Override public void setSelection(final int index) { stackedPane.showPane(index, false); } }) { @Override public void startTransition(int fromIndex, int toIndex, double direction) { if (IS_LINUX_OS) { Control from = transitionable.getControl(fromIndex); Rectangle fromSize = from.getBounds(); Image imgFrom = new Image(from.getDisplay(), fromSize.width, fromSize.height); GC gcfrom = new GC(from); from.update(); gcfrom.copyArea(imgFrom, 0, 0); if (compImage[fromIndex - 1] != null) { compImage[fromIndex - 1].dispose(); } compImage[fromIndex - 1] = imgFrom; gcfrom.dispose(); } super.startTransition(fromIndex, toIndex, direction); } }; transitionManager.addTransitionListener(new TransitionListener() { @Override public void transitionFinished(final TransitionManager arg0) { stackedPane.completeSelection(); } }); transitionManager.setControlImages(compImage); // new SlideTransition(_tm) transitionManager.setTransition(new CubicRotationTransition( transitionManager)); // listen out for closing parent.addDisposeListener(this); // and remember to detach ourselves final DisposeListener meL = this; final Runnable dropMe = new Runnable() { @Override public void run() { parent.removeDisposeListener(meL); } }; addRunOnCloseCallback(dropMe); // ok, see if we have a time controller findTimeController(); } private void findTimeController() { IExtensionRegistry registry = Platform.getExtensionRegistry(); if (registry != null) { final IExtensionPoint point = Platform.getExtensionRegistry().getExtensionPoint( Activator.PLUGIN_ID, TIME_PROVIDER_ID); final IExtension[] extensions = point.getExtensions(); for (int i = 0; i < extensions.length; i++) { final IExtension iExtension = extensions[i]; final IConfigurationElement[] confE = iExtension.getConfigurationElements(); for (IConfigurationElement extension : confE) { try { final IStackedTimeProvider provider = (IStackedTimeProvider) extension .createExecutableExtension("class"); final IStackedTimeListener listener = this; // ok, can it provide time control? if (provider.canProvideControl()) { provider.controlThis(listener); // ok, remember that we need to close this addRunOnCloseCallback(new Runnable() { @Override public void run() { provider.releaseThis(listener); } }); } } catch (final CoreException ex) { ex.printStackTrace(); } } } } // // // IConfigurationElement[] config = // Platform.getExtensionRegistry().getConfigurationElementsFor( // TIME_PROVIDER_ID); // for (IConfigurationElement e : config) // { // Object o; // try // { // o = e.createExecutableExtension("class"); // if (o instanceof IStackedTimeProvider) // { // final IStackedTimeProvider sa = (IStackedTimeProvider) o; // final IStackedTimeListener listener = this; // // // ok, can it provide time control? // if(sa.canProvideControl()) // { // sa.controlThis(listener); // // // ok, remember that we need to close this // addRunOnCloseCallback(new Runnable() // { // @Override // public void run() // { // sa.releaseThis(listener); // } // }); // } // } // } // catch (CoreException e1) // { // e1.printStackTrace(); // } // } } /** * Creates an Chart Set with a single chart so that user would be able to drop datasets in it. * * @return */ private ChartSet createBlankModel() { ChartSet chartSet = StackedchartsFactory.eINSTANCE.createChartSet(); chartSet.getCharts().add(StackedchartsFactory.eINSTANCE.createChart()); IndependentAxis independentAxis = StackedchartsFactory.eINSTANCE.createIndependentAxis(); independentAxis .setAxisType(StackedchartsFactory.eINSTANCE.createDateAxis()); chartSet.setSharedAxis(independentAxis); return chartSet; } protected void fillLocalPullDown(final IMenuManager manager) { } protected void fillLocalToolBar(final IToolBarManager manager) { String actionText = stackedPane.getActiveControlKey() == CHART_VIEW ? "Edit" : "View"; Action toggleViewModeAction = new Action(actionText, SWT.TOGGLE) { @Override public void run() { if (stackedPane.getActiveControlKey() == CHART_VIEW) { selectView(EDIT_VIEW); setText("View"); manager.update(true); } else { // recreate the model // TODO: let's not re-create the model each time we revert // to the view mode. let's create listeners, so the // chart has discrete updates in response to // model changes // double check we have a charts model if (charts != null) { setModel(charts); } selectView(CHART_VIEW); setText("Edit"); manager.update(true); } } }; manager.add(toggleViewModeAction); final Action showTime = new Action("Show time marker", SWT.TOGGLE) { @Override public void run() { // ok, trigger graph redraw final JFreeChart combined = _chartComposite.getChart(); final TimeBarPlot plot = (TimeBarPlot) combined.getPlot(); plot._showLine = isChecked(); // ok, trigger ui update refreshPlot(); } }; showTime.setChecked(true); showTime.setImageDescriptor(AbstractUIPlugin.imageDescriptorFromPlugin( Activator.PLUGIN_ID, "icons/clock.png")); manager.add(showTime); final Action showMarker = new Action("Show marker value", SWT.TOGGLE) { @Override public void run() { // ok, trigger graph redraw final JFreeChart combined = _chartComposite.getChart(); final TimeBarPlot plot = (TimeBarPlot) combined.getPlot(); plot._showLabels = isChecked(); // ok, trigger ui update refreshPlot(); } }; showMarker.setChecked(true); showMarker.setImageDescriptor(AbstractUIPlugin.imageDescriptorFromPlugin( Activator.PLUGIN_ID, "icons/labels.png")); manager.add(showMarker); final Action export = new Action("Export image to clipboard", SWT.PUSH) { @Override public void run() { toWMF(); } private void toWMF() { try { final Clipboard clpbrd = Toolkit.getDefaultToolkit().getSystemClipboard(); clpbrd.setContents(new DrawableWMFTransfer( _chartComposite.getChart(), _chartComposite.getBounds()), null); MessageDialog.openInformation(Display.getCurrent().getActiveShell(), "Image Export", "Exported to Clipboard in WMF && PDF format"); } catch (final Exception e) { e.printStackTrace(); } } }; export.setImageDescriptor(AbstractUIPlugin.imageDescriptorFromPlugin( Activator.PLUGIN_ID, "icons/export_wmf.png")); manager.add(export); final Action sizeDown = new Action("-", IAction.AS_PUSH_BUTTON) { @Override public void run() { changeFontSize(false); } }; sizeDown.setImageDescriptor(AbstractUIPlugin.imageDescriptorFromPlugin( Activator.PLUGIN_ID, "icons/decrease.png")); sizeDown.setDescription("Decrease font size"); manager.add(sizeDown); final Action sizeUp = new Action("+", IAction.AS_PUSH_BUTTON) { @Override public void run() { changeFontSize(true); } }; sizeUp.setDescription("Increase font size"); sizeUp.setImageDescriptor(AbstractUIPlugin.imageDescriptorFromPlugin( Activator.PLUGIN_ID, "icons/increase.png")); manager.add(sizeUp); } /** * View Selection provider where it proxy between selected view */ protected void fireSelectionChnaged() { final ISelection selection = getSelection(); for (final ISelectionChangedListener listener : new ArrayList<>( selectionListeners)) { listener.selectionChanged(new SelectionChangedEvent(this, selection)); } } @Override @SuppressWarnings("rawtypes") public Object getAdapter(final Class type) { if (type == CommandStack.class) { return chartEditor.getViewer().getEditDomain().getCommandStack(); } if (type == IPropertySheetPage.class) { return chartEditor.getPropertySheetPage(); } return super.getAdapter(type); } /** * accessor, to be used in exporting the image * * @return */ public ChartComposite getChartComposite() { return _chartComposite; } @Override public String getContributorId() { return getViewSite().getId(); } @Override public ISelection getSelection() { if (!initEditor.get() && stackedPane.getActiveControlKey() == EDIT_VIEW) { return chartEditor.getSelection(); } // if chart view need to provide selection info via properties view, change empty selection to // represent object of chart view selection. return new StructuredSelection();// empty selection } protected void handleDoubleClick(final int x, final int y) { // retrieve the data location final Rectangle dataArea = _chartComposite.getScreenDataArea(); final Rectangle2D d2 = new Rectangle2D.Double(dataArea.x, dataArea.y, dataArea.width, dataArea.height); final TimeBarPlot plot = (TimeBarPlot) _chartComposite.getChart().getPlot(); final double chartX = plot.getDomainAxis().java2DToValue(x, d2, plot.getDomainAxisEdge()); // do we have a date to control? if (_controllableDate != null) { // ok, update it _controllableDate.setDate(new Date((long) chartX)); } } private void initEditorViewModel() { if (initEditor.getAndSet(false)) { chartEditor.setModel(charts); } editorHolder.pack(true); editorHolder.getParent().layout(); } private void refreshPlot() { final Runnable runnable = new Runnable() { @Override public void run() { if (_chartComposite != null && !_chartComposite.isDisposed()) { final JFreeChart c = _chartComposite.getChart(); if (c != null) { c.setNotify(true); } } } }; if (Display.getCurrent() != null) { runnable.run(); } else { Display.getDefault().syncExec(runnable); } } @Override public void removeSelectionChangedListener( final ISelectionChangedListener listener) { selectionListeners.remove(listener); } public void selectView(final int view) { if (stackedPane != null && !stackedPane.isDisposed()) { // if switch to edit mode make sure to init editor model if (view == EDIT_VIEW) { initEditorViewModel(); } stackedPane.showPane(view); // fire selection change to refresh properties view fireSelectionChnaged(); } } public void setDateSupport(final ControllableDate controllableDate) { _controllableDate = controllableDate; if (_controllableDate != null) { final Date theDate = _controllableDate.getDate(); if (theDate != null) { updateTime(theDate); } } } @Override public void setFocus() { if (stackedPane != null && !stackedPane.isDisposed()) { stackedPane.forceFocus(); } } public void setModel(final ChartSet charts) { setModel(charts, CHART_VIEW); } public void setModel(final ChartSet charts, int mode) { this.charts = charts; // mark editor to recreate initEditor.set(true); // remove any existing base items on view holder if (chartHolder != null) { for (final Control control : chartHolder.getChildren()) { control.dispose(); } } // and now repopulate jFreeChart = ChartBuilder.build(charts, _controllableDate); // initialise the theme changeFontSize(null); _chartComposite = new ChartComposite(chartHolder, SWT.NONE, jFreeChart, 400, 600, 300, 200, 1800, 1800, true, false, true, true, true, true) { @Override public void mouseUp(final MouseEvent event) { super.mouseUp(event); final JFreeChart c = getChart(); if (c != null) { c.setNotify(true); // force redraw } if (event.count == 2) { handleDoubleClick(event.x, event.y); } } }; jFreeChart.setAntiAlias(false); // try the double-click handler _chartComposite.addMouseListener(new MouseListener() { @Override public void mouseDoubleClick(final MouseEvent e) { System.out.println("double-click at:" + e); } @Override public void mouseDown(final MouseEvent e) { System.out.println("down at:" + e); } @Override public void mouseUp(final MouseEvent e) { System.out.println("up at:" + e); } }); chartHolder.pack(true); chartHolder.getParent().layout(); selectView(mode); } @Override public void setSelection(final ISelection selection) { if (!initEditor.get()) { chartEditor.getViewer().setSelection(selection); } } /** * update (or clear) the displayed time marker * * @param newTime */ @Override public void updateTime(final Date newTime) { final Date oldTime = _currentTime; _currentTime = newTime; if (newTime != null && !newTime.equals(oldTime) || newTime != oldTime) { if (!_amUpdating) { // ok, remember that we're updating _amUpdating = true; // remember the new one _pendingTime.set(newTime.getTime()); // get on with the update try { Display.getDefault().asyncExec(new Runnable() { @Override public void run() { // quick, capture the time final long safeTime = _pendingTime.get(); // do we have a pending time value if (safeTime != INVALID_TIME) { _pendingTime.set(INVALID_TIME); // now create the time object final Date theDTG = new Date(safeTime); // try to get the time aware plot, if we have one. // it may be null if we're blank. if (_chartComposite != null) { final JFreeChart combined = _chartComposite.getChart(); final TimeBarPlot plot = (TimeBarPlot) combined.getPlot(); plot.setTime(theDTG); // ok, trigger ui update refreshPlot(); } } else { // ok, there isn't a pending date, we can just skip the update } // Note: we don't need to clear the lock, we do it in the finally block } }); } finally { // clear the updating lock _amUpdating = false; } } } } @Override public void widgetDisposed(final DisposeEvent e) { if (_closeCallbacks != null) { for (final Runnable callback : _closeCallbacks) { callback.run(); } } // and remove ourselves from our parent } }