/*******************************************************************************
* Copyright (c) 2009 the CHISEL group and contributors.
* 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:
* Del Myers -- initial API and implementation
*******************************************************************************/
package org.eclipse.zest.custom.sequence.widgets;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.TreeMap;
import java.util.TreeSet;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.TypedListener;
import org.eclipse.zest.custom.sequence.events.internal.ListenerList;
import org.eclipse.zest.custom.sequence.widgets.internal.IWidgetProperties;
/**
* A composite that is used to display UML charts.
* @author Del Myers
*/
public abstract class UMLChart extends Composite {
// an id that is used for quick sorting
private int nextId = 0;
TreeSet<UMLItem> items;
//registers styled fonts for universal use.
private TreeMap<Integer, Font> fontRegistry;
//holds fonts that are no longer of use.
private List<Font> deadFonts;
private ListenerList listeners;
/**
* Indicates that some sort of structural change has occurred and the viewer must refresh itself.
*/
boolean dirty;
private UMLItem[] selectedItems;
private Composite contents;
/**
* @param parent
* @param style
*/
public UMLChart(Composite parent, int style) {
super(parent, style);
this.selectedItems = new UMLItem[0];
fontRegistry = new TreeMap<Integer, Font>();
deadFonts = new LinkedList<Font>();
nextId = 0;
this.items = new TreeSet<UMLItem>(new Comparator<UMLItem>(){
public int compare(UMLItem o1, UMLItem o2) {
return o1.getid() - o2.getid();
}
});
listeners = new ListenerList();
dirty = false;
this.contents = createContents(this, style);
setLayout(new FillLayout());
addDisposeListener(new DisposeListener(){
public void widgetDisposed(DisposeEvent e) {
UMLChart.this.widgetDisposed(e);
}
});
}
/**
* Creates the contents for this chart. Delegated to subclasses.
* @param parent the parent of the contents.
* @return the composite that is used to display the contents.
*/
protected abstract Composite createContents(Composite parent, int style);
/**
* @param e
*/
protected void widgetDisposed(DisposeEvent e) {
UMLItem[] items = getItems();
//clear the items from the list so that we don't waste time deleting them
//on the dispose.
this.items.clear();
for (UMLItem item : items) {
item.dispose();
}
}
/**
* Gets the next item id for an item placed on this chart.
* @return
*/
int getNextId() {
return nextId++;
}
void createItem(UMLItem item) {
item.setid(getNextId());
this.items.add(item);
this.dirty = true;
firePropertyChange(IWidgetProperties.ITEM, null, item);
}
void deleteItem(UMLItem item) {
if (items.size() == 0) {
//don't waste time disposing.
return;
}
this.items.remove(item);
//check for the item in the selection and remove it.
UMLItem[] currentSelection = getSelection();
int i = 0;
for (; i < currentSelection.length; i++) {
if (currentSelection[i] == item) {
break;
}
}
if (i < currentSelection.length) {
UMLItem[] newSelection = new UMLItem[currentSelection.length-1];
if (i > 0) {
System.arraycopy(currentSelection, 0, newSelection, 0, i-1);
}
if (i < newSelection.length) {
System.arraycopy(currentSelection, i+1, newSelection, i, newSelection.length-i);
}
setSelection(newSelection);
}
this.dirty = true;
firePropertyChange(IWidgetProperties.ITEM, item, null);
}
/**
* Convenience method for disposing all of the items on this chart and refreshing.
*
*
*/
public void clearChart() {
for (UMLItem item : getItems()) {
item.dispose();
}
dirty = true;
layout();
}
/**
* @return the items
*/
public UMLItem[] getItems() {
return items.toArray(new UMLItem[items.size()]);
}
/**
* Returns the number of items in the chart.
* @return the number of items in the chart.
*/
public int itemCount() {
return items.size();
}
/* (non-Javadoc)
* @see org.eclipse.swt.widgets.Control#setBackground(org.eclipse.swt.graphics.Color)
*/
@Override
public void setBackground(Color color) {
contents.setBackground(color);
}
/**
* Adds the given property change listener to the list of listeners if it hasn't already been added.
* @param listener
*/
public void addPropertyChangeListener(PropertyChangeListener listener) {
listeners.add(listener);
}
/**
* Removes the given listener from the list of listeners.
* @param listener
*/
public void removePropertyChangeListener(PropertyChangeListener listener) {
listeners.remove(listener);
}
/**
* Sets the dirty state of this chart to true. This will ensure that when the chart is next
* layed-out, it will be structurally refreshed.
*
*/
protected void markDirty() {
this.dirty = true;
}
protected boolean isDirty() {
return this.dirty;
}
/**
* Fires a property change for the given property.
* @param property
* @param oldValue
* @param newValue
*/
void firePropertyChange(String property, Object oldValue, Object newValue) {
if (!shouldFire(oldValue, newValue)) return;
for (Object listener : listeners.getListeners()) {
((PropertyChangeListener)listener).propertyChanged(this, property, oldValue, newValue);
}
}
boolean shouldFire(Object oldValue, Object newValue) {
if (oldValue != null || newValue != null) {
return (oldValue != null) ? !oldValue.equals(newValue) : !newValue.equals(oldValue);
}
return false;
}
/**
* Lays out the chart. Because a chart may have a lot of children on it, an explicit refresh() or
* layout() must be called after a child is added or disposed in order to see the results.
*/
@Override
public void layout(boolean changed) {
if (changed) {
refresh();
}
performLayout();
super.layout(changed);
}
/**
* Refreshes the visuals of this chart.
*/
public abstract void refresh();
/**
* Returns a universal font with the given style. Fonts will be disposed automatically.
*/
public Font getFont(int style) {
int mask = SWT.BOLD | SWT.ITALIC;
style &= mask;
if ((style ^ SWT.NORMAL) == 0) {
return getFont();
}
Font f = fontRegistry.get(style);
if (f == null) {
FontData[] data = getFont().getFontData();
data[0].setStyle(style);
f = new Font(getDisplay(), data[0]);
fontRegistry.put(style, f);
}
return f;
}
/* (non-Javadoc)
* @see org.eclipse.swt.widgets.Canvas#setFont(org.eclipse.swt.graphics.Font)
*/
@Override
public void setFont(Font font) {
for (Font f : fontRegistry.values()) {
//store the fonts until disposal just in case a child is still referencing that font.
deadFonts.add(f);
fontRegistry.clear();
}
Font old = getFont();
super.setFont(font);
firePropertyChange("fnt", old, font);
}
public void setSelection(UMLItem[] items) {
checkWidget();
if (items == null) SWT.error(SWT.ERROR_NULL_ARGUMENT);
for (UMLItem item : items) {
if (item == null) SWT.error(SWT.ERROR_NULL_ARGUMENT);
if (item.getChart()!=this) SWT.error(SWT.ERROR_INVALID_PARENT);
}
this.selectedItems = new UMLItem[items.length];
System.arraycopy(items, 0, this.selectedItems, 0, items.length);
}
/**
* Sets the selection and fires an event.
* @param items
*/
protected void internalSetSelection(UMLItem[] items) {
HashSet<UMLItem> oldSelection = new HashSet<UMLItem>(Arrays.asList(this.selectedItems));
setSelection(items);
// send an event for each new selection
if (oldSelection.size() != 0 && items.length == 0) {
//clear the selection
Event event = new Event();
event.widget = this;
event.item = null;
int type = SWT.Selection;
event.type = type;
notifyListeners(type, event);
} else {
for (UMLItem item : items) {
Event event = new Event();
event.widget = this;
event.item = item;
int type = SWT.Selection;
if (oldSelection.contains(item)) {
type = SWT.DefaultSelection;
}
event.type = type;
notifyListeners(type, event);
}
}
}
protected void internalUpdateSelection(UMLItem[] items) {
HashSet<UMLItem> oldSelection = new HashSet<UMLItem>(Arrays.asList(this.selectedItems));
setSelection(items);
for (UMLItem item : items) {
if (!oldSelection.contains(item)) {
Event event = new Event();
event.widget = this;
event.item = item;
int type = SWT.Selection;
event.type = type;
notifyListeners(type, event);
}
}
}
protected void internalReselect(UMLItem item) {
HashSet<UMLItem> oldSelection = new HashSet<UMLItem>(Arrays.asList(this.selectedItems));
if (oldSelection.contains(item)) {
Event event = new Event();
event.widget = this;
event.item = item;
int type = SWT.DefaultSelection;
event.type = type;
notifyListeners(type, event);
}
}
public UMLItem[] getSelection() {
return selectedItems;
}
public void addSelectionListener(SelectionListener listener) {
checkWidget();
if (listener == null) SWT.error (SWT.ERROR_NULL_ARGUMENT);
TypedListener typedListener = new TypedListener (listener);
addListener (SWT.Selection,typedListener);
}
public void removeSelectionListener(SelectionListener listener) {
removeListener(SWT.Selection, listener);
}
protected abstract void performLayout();
/**
* Whether an item is visible or not is decided by the chart.
* @param item
* @return
*/
abstract boolean isVisible(UMLItem item);
}