package org.nightlabs.jfire.issuetracking.ui.issuemarker; import java.io.ByteArrayInputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import javax.jdo.FetchPlan; import javax.jdo.JDOHelper; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.ListenerList; import org.eclipse.core.runtime.Status; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.ISelectionProvider; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.ScrolledComposite; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Group; import org.eclipse.swt.widgets.Label; import org.nightlabs.base.ui.job.Job; import org.nightlabs.jdo.NLJDOHelper; import org.nightlabs.jfire.issue.Issue; import org.nightlabs.jfire.issue.issuemarker.IssueMarker; import org.nightlabs.jfire.issue.issuemarker.IssueMarkerDAO; import org.nightlabs.jfire.issue.issuemarker.id.IssueMarkerID; import org.nightlabs.progress.ProgressMonitor; import org.nightlabs.util.NLLocale; /** * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de */ public class IssueMarkerToggleButtonBar extends Composite implements ISelectionProvider { private static final String[] FETCH_GROUPS_ISSUE_MARKER = { FetchPlan.DEFAULT, IssueMarker.FETCH_GROUP_NAME, IssueMarker.FETCH_GROUP_DESCRIPTION, IssueMarker.FETCH_GROUP_ICON_16X16_DATA }; private final Display display; private int flags; private boolean useGroup = true; private Group group; private ScrolledComposite scrolledComposite; private Composite scrolledCompositeContent; private Set<IssueMarkerID> selectedIssueMarkerIDs = Collections.emptySet(); private List<IssueMarker> issueMarkers; private Map<IssueMarkerID, IssueMarker> issueMarkerID2issueMarker; private Map<IssueMarker, Image> issueMarker2image; private Map<Button, IssueMarker> button2issueMarker; private Map<IssueMarker, Button> issueMarker2button; private void createGroup() { if (group != null && !group.isDisposed()) group.dispose(); group = null; if (useGroup) { // group = new Group(this, SWT.NONE + SWT.BORDER); // With or without border, at least on Linux, it looks the same ;-) Marco. group = new Group(this, SWT.NONE); // But NOT under windows, Daniel group.setLayout(new FillLayout()); group.setText("Markers"); } } /** * Convienience constructor calling {@link #IssueMarkerToggleButtonBar(Composite, int)} with <code>flags = SWT.H_SCROLL</code>. * @param parent the container-UI-element. */ public IssueMarkerToggleButtonBar(Composite parent) { this(parent, SWT.H_SCROLL); } public void setUseGroup(boolean useGroup) { this.useGroup = useGroup; } public boolean isUseGroup() { return useGroup; } protected boolean isVerticalLayout() { return (SWT.V_SCROLL & flags) != 0; } /** * Create a new UI element for selecting (toggling) {@link IssueMarker}s. * * @param parent the container-UI-element. * @param flags Can be {@link SWT#H_SCROLL} for horizontal layout or {@link SWT#V_SCROLL} for vertical layout. */ public IssueMarkerToggleButtonBar(Composite parent, int flags) { super(parent, SWT.NONE); this.flags = flags; if (isVerticalLayout()) this.setLayoutData(new GridData(GridData.FILL_VERTICAL)); else this.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); this.setLayout(new FillLayout()); display = getDisplay(); addDisposeListener(new DisposeListener() { @Override public void widgetDisposed(DisposeEvent event) { disposeChildElements(); } }); Label loadingMessage = new Label(group == null ? this : group, SWT.NONE); loadingMessage.setText("Loading..."); Job job = new Job("Loading issue markers") { @Override protected IStatus run(ProgressMonitor monitor) throws Exception { final List<IssueMarker> _issueMarkers = IssueMarkerDAO.sharedInstance().getIssueMarkers( FETCH_GROUPS_ISSUE_MARKER, NLJDOHelper.MAX_FETCH_DEPTH_NO_LIMIT, monitor ); final Map<IssueMarkerID, IssueMarker> _issueMarkerID2issueMarker = new HashMap<IssueMarkerID, IssueMarker>(); for (IssueMarker issueMarker : _issueMarkers) { IssueMarkerID issueMarkerID = (IssueMarkerID) JDOHelper.getObjectId(issueMarker); if (issueMarkerID == null) throw new IllegalStateException("JDOHelper.getObjectId(issueMarker) returned null for: " + issueMarker); _issueMarkerID2issueMarker.put(issueMarkerID, issueMarker); } display.asyncExec(new Runnable() { @Override public void run() { issueMarkers = _issueMarkers; issueMarkerID2issueMarker = _issueMarkerID2issueMarker; createToggleButtons(); } }); return Status.OK_STATUS; } }; job.setPriority(Job.SHORT); job.schedule(); } private void disposeChildElements() { if (Display.getCurrent() == null) throw new IllegalStateException("Thread mismatch! This method must be called on the SWT UI thread!"); if (issueMarker2button != null) { for (Map.Entry<IssueMarker, Button> me : issueMarker2button.entrySet()) { me.getValue().dispose(); } issueMarker2button = null; } if (issueMarker2image != null) { for (Map.Entry<IssueMarker, Image> me : issueMarker2image.entrySet()) { me.getValue().dispose(); } issueMarker2image = null; } if (!isDisposed()) { for (Control c : getChildren()) c.dispose(); } scrolledCompositeContent = null; // already disposed by the getChildren iteration before scrolledComposite = null; // already disposed by the getChildren iteration before button2issueMarker = null; } private void createToggleButtons() { disposeChildElements(); final Locale locale = NLLocale.getDefault(); createGroup(); int scrolledCompFlags = flags & (SWT.H_SCROLL | SWT.V_SCROLL); scrolledComposite = new ScrolledComposite(group == null ? this : group, scrolledCompFlags); // | SWT.BORDER); // with or without border - not sure what looks better (especially I have no idea how it looks on Windows). Marco. scrolledCompositeContent = new Composite(scrolledComposite, SWT.NONE); scrolledCompositeContent.setLayout(new GridLayout(1, true)); scrolledComposite.setContent(scrolledCompositeContent); issueMarker2button = new HashMap<IssueMarker, Button>(); button2issueMarker = new HashMap<Button, IssueMarker>(); issueMarker2image = new HashMap<IssueMarker, Image>(); for (IssueMarker issueMarker : issueMarkers) { Image image = null; if (issueMarker.getIcon16x16Data() != null) { ImageData imageData = new ImageData(new ByteArrayInputStream(issueMarker.getIcon16x16Data())); // No need to close this stream, because it is purely in-memory. image = new Image(display, imageData); issueMarker2image.put(issueMarker, image); } Button button = new Button(scrolledCompositeContent, SWT.TOGGLE); if (image != null) button.setImage(image); else button.setText(issueMarker.getName().getText(locale)); button.setToolTipText( String.format("*** %s ***\n%s", issueMarker.getName().getText(locale), issueMarker.getDescription().getText(locale)) ); button.addSelectionListener(toggleButtonSelectionListener); issueMarker2button.put(issueMarker, button); button2issueMarker.put(button, issueMarker); } if (isVerticalLayout()) ((GridLayout)scrolledCompositeContent.getLayout()).numColumns = 1; else ((GridLayout)scrolledCompositeContent.getLayout()).numColumns = Math.max(1, issueMarker2button.size()); scrolledCompositeContent.setSize(scrolledCompositeContent.computeSize(SWT.DEFAULT, SWT.DEFAULT)); getParent().layout(true, true); // the height of this whole composite might change depending on the size of the buttons. updateUI(); } private SelectionListener toggleButtonSelectionListener = new SelectionAdapter() { @Override public void widgetSelected(org.eclipse.swt.events.SelectionEvent event) { IssueMarker issueMarker = button2issueMarker.get(event.getSource()); if (issueMarker == null) throw new IllegalStateException("No IssueMarker registered for this event source: " + event.getSource()); IssueMarkerID issueMarkerID = (IssueMarkerID) JDOHelper.getObjectId(issueMarker); if (issueMarkerID == null) throw new IllegalStateException("JDOHelper.getObjectId(issueMarker) returned null for: " + issueMarker); Set<IssueMarkerID> tmpIDs = new HashSet<IssueMarkerID>(selectedIssueMarkerIDs); Button button = (Button) event.getSource(); if (button.getSelection()) tmpIDs.add(issueMarkerID); else tmpIDs.remove(issueMarkerID); selectedIssueMarkerIDs = Collections.unmodifiableSet(tmpIDs); fireSelectionChangedListeners(); } }; private ListenerList selectionChangedListeners = new ListenerList(); /** * Get the selection, i.e. the OIDs of those {@link IssueMarker}s that were toggled into state "selected" by the user * (or by a call to {@link #setSelection(ISelection)} / {@link #setSelectedIssueMarkerIDs(Collection)}). * <p> * This method never returns <code>null</code>. * </p> * * @return OID-representations of the selected {@link IssueMarker}s - never <code>null</code>. */ public Set<IssueMarkerID> getSelectedIssueMarkerIDs() { return selectedIssueMarkerIDs; // is already unmodifiable. } /** * Get the selected {@link IssueMarker} instances or <code>null</code> before the data is loaded. * This is a convenience method to convert the IDs to instances of {@link IssueMarker}. * * @return <code>null</code> or the selected {@link IssueMarker}s. * @see #getSelectedIssueMarkerIDs() */ public Set<IssueMarker> getSelectedIssueMarkers() { Set<IssueMarkerID> ids = selectedIssueMarkerIDs; Map<IssueMarkerID, IssueMarker> map = issueMarkerID2issueMarker; if (map == null) return null; Set<IssueMarker> result = new HashSet<IssueMarker>(ids.size()); for (IssueMarkerID issueMarkerID : ids) { IssueMarker issueMarker = map.get(issueMarkerID); if (issueMarker == null) throw new IllegalStateException("issueMarkerID2issueMarker.get(issueMarkerID) returned null for " + issueMarkerID); result.add(issueMarker); } return Collections.unmodifiableSet(result); } /** * Convenience method which implicitely converts from {@link IssueMarker}s to their OIDs and * then calls {@link #setSelectedIssueMarkerIDs(Collection)}. * * @param selectedIssueMarkers the currently selected {@link IssueMarker}s. * @see #setSelectedIssueMarkerIDs(Collection) */ public void setSelectedIssueMarkers(Collection<? extends IssueMarker> selectedIssueMarkers) { List<IssueMarkerID> ids = null; if (selectedIssueMarkers != null) ids = NLJDOHelper.getObjectIDList(selectedIssueMarkers); setSelectedIssueMarkerIDs(ids); } /** * Set the currently selected {@link IssueMarker}s via their object-ids (instances of {@link IssueMarkerID}). * * @param selectedIssueMarkerIDs the OIDs of the new selection. */ public void setSelectedIssueMarkerIDs(Collection<IssueMarkerID> selectedIssueMarkerIDs) { if (Display.getCurrent() == null) throw new IllegalStateException("Thread mismatch! This method must be called on the SWT UI thread!"); if (selectedIssueMarkerIDs == null || selectedIssueMarkerIDs.isEmpty()) this.selectedIssueMarkerIDs = Collections.emptySet(); else this.selectedIssueMarkerIDs = Collections.unmodifiableSet(new HashSet<IssueMarkerID>(selectedIssueMarkerIDs)); updateUI(); fireSelectionChangedListeners(); } protected void updateUI() { if (Display.getCurrent() == null) throw new IllegalStateException("Thread mismatch! This method must be called on the SWT UI thread!"); if (issueMarker2button != null) { for (Map.Entry<IssueMarker, Button> me : issueMarker2button.entrySet()) { IssueMarkerID issueMarkerID = (IssueMarkerID) JDOHelper.getObjectId(me.getKey()); me.getValue().setSelection(selectedIssueMarkerIDs.contains(issueMarkerID)); } } } protected void fireSelectionChangedListeners() { if (Display.getCurrent() == null) throw new IllegalStateException("Thread mismatch! This method must be called on the SWT UI thread!"); Object[] listeners = selectionChangedListeners.getListeners(); if (listeners.length == 0) return; SelectionChangedEvent event = new SelectionChangedEvent(this, getSelection()); for (Object l : listeners) { ((ISelectionChangedListener)l).selectionChanged(event); } } @Override public void addSelectionChangedListener(ISelectionChangedListener listener) { selectionChangedListeners.add(listener); } /** * {@inheritDoc} * <p> * The implementation in {@link IssueMarkerToggleButtonBar} returns an instance of {@link IStructuredSelection} * containing instances of {@link IssueMarkerID}. * </p> * @see #setSelection(ISelection) * @see #getSelectedIssueMarkerIDs() * @see #getSelectedIssueMarkers() */ @Override public ISelection getSelection() { return new StructuredSelection(getSelectedIssueMarkerIDs().toArray()); } @Override public void removeSelectionChangedListener(ISelectionChangedListener listener) { selectionChangedListeners.remove(listener); } /** * {@inheritDoc} * <p> * The implementation in {@link IssueMarkerToggleButtonBar} accepts only instances of {@link IStructuredSelection} * that contain {@link IssueMarker}s or {@link IssueMarkerID}s (they can be mixed). * </p> * @see #getSelection() * @see #setSelectedIssueMarkerIDs(Collection) * @see #setSelectedIssueMarkers(Collection) */ @Override public void setSelection(ISelection selection) { IStructuredSelection sel = (IStructuredSelection) selection; Set<IssueMarkerID> issueMarkerIDs = new HashSet<IssueMarkerID>(); Set<IssueMarker> issueMarkers = new HashSet<IssueMarker>(); for (Iterator<?> it = sel.iterator(); it.hasNext(); ) { Object o = it.next(); if (o == null) ; // silently ignore else if (o instanceof IssueMarkerID) issueMarkerIDs.add((IssueMarkerID) o); else if (o instanceof IssueMarker) issueMarkers.add((IssueMarker) o); else throw new IllegalArgumentException("Selection contains element of type \"" + o.getClass().getName() + "\" which is not supported! Only IssueMarkerID and IssueMarker are accepted! Element: " + o); } if (!issueMarkers.isEmpty()) { List<IssueMarkerID> ids = NLJDOHelper.getObjectIDList(issueMarkers); issueMarkerIDs.addAll(ids); } setSelectedIssueMarkerIDs(issueMarkerIDs); } /** * Apply the current selection to an issue. This is probably the most common use case for this * UI element. * * @param issue the target of the copy operation, which must not be <code>null</code>. */ public void commitToIssue(Issue issue) { if (issue == null) throw new IllegalArgumentException("issue == null"); Set<IssueMarker> selectedIssueMarkers = this.getSelectedIssueMarkers(); // If the selectedIssueMarkers are null, it means the data was not yet loaded. // If we commit now, we can be sure that the user did not make any change, because he did not see any UI, yet. // Thus, we silently leave. if (selectedIssueMarkers == null) return; // First, we remove all those IssueMarkers from the Issue that are no longer selected. for (IssueMarker issueMarker : new ArrayList<IssueMarker>(issue.getIssueMarkers())) { if (!selectedIssueMarkers.contains(issueMarker)) issue.removeIssueMarker(issueMarker); } // Then we add all currently selected ones (this should only modify the set - hopefully - if it is not yet an element). for (IssueMarker issueMarker : selectedIssueMarkers) issue.addIssueMarker(issueMarker); } // /** // * Get the {@link IssueMarker} instances mapped by their OIDs or <code>null</code> before the data was loaded. // * // * @return <code>null</code> or a <code>Map</code> containing all {@link IssueMarker}s mapped by their OIDs. // */ // public Map<IssueMarkerID, IssueMarker> getIssueMarkerID2issueMarker() { // Map<IssueMarkerID, IssueMarker> issueMarkerID2issueMarker = this.issueMarkerID2issueMarker; // if (issueMarkerID2issueMarker == null) // return null; // else // return Collections.unmodifiableMap(issueMarkerID2issueMarker); // } }