/*******************************************************************************
* Copyright 2005-2006, CHISEL Group, University of Victoria, Victoria, BC, Canada.
* 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:
* The Chisel Group, University of Victoria
*******************************************************************************/
package net.sourceforge.tagsea.core.ui.tags;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import net.sourceforge.tagsea.TagSEAPlugin;
import net.sourceforge.tagsea.core.ITag;
import net.sourceforge.tagsea.core.ITagChangeEvent;
import net.sourceforge.tagsea.core.ITagChangeListener;
import net.sourceforge.tagsea.core.TagDelta;
import org.eclipse.jface.fieldassist.IContentProposal;
import org.eclipse.jface.fieldassist.IContentProposalProvider;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.widgets.Control;
/**
* A content proposal provider that is hooked to a parent control, and will become invalid
* after that control is disposed.
* @author Del Myers
*/
public class TagProposalProvider implements IContentProposalProvider {
private ITag[] tags;
private Control parentControl;
private TagChangeListener tagListener;
private class TagChangeListener implements ITagChangeListener {
/* (non-Javadoc)
* @see net.sourceforge.tagsea.core.ITagChangeListener#tagsChanged(net.sourceforge.tagsea.core.TagDelta)
*/
public void tagsChanged(TagDelta delta) {
//no need to do anything if the tags are already null.
if (TagProposalProvider.this.tags == null) return;
for (ITagChangeEvent event : delta.events) {
if (isRelatedType(event)) {
TagProposalProvider.this.tags = null;
break;
}
}
}
private boolean isRelatedType(ITagChangeEvent event) {
int type = event.getType();
return (type != ITagChangeEvent.WAYPOINTS);
}
}
private class TagComparator implements Comparator<ITag> {
/* (non-Javadoc)
* @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
*/
public int compare(ITag o1, ITag o2) {
String name1 = o1.getName().toLowerCase();
String name2 = o2.getName().toLowerCase();
return name1.compareTo(name2);
}
}
public TagProposalProvider(Control parentControl) {
this.tags = null;
this.parentControl = parentControl;
this.tagListener = new TagChangeListener();
TagSEAPlugin.addTagChangeListener(tagListener);
hookControl();
}
/**
* hooks this provider to the control so that we can stop listening for tag changes.
*/
private void hookControl() {
parentControl.addDisposeListener(new DisposeListener(){
public void widgetDisposed(DisposeEvent e) {
TagSEAPlugin.removeTagChangeListener(tagListener);
}
});
}
/**
* Gets all tags and sorts them according to case-insensitive names.
*/
public synchronized void refreshTags() {
ArrayList<ITag> tagList = new ArrayList<ITag>(
Arrays.asList(TagSEAPlugin.getTagsModel().getAllTags())
);
Collections.sort(tagList, new TagComparator());
this.tags = tagList.toArray(new ITag[tagList.size()]);
}
/* (non-Javadoc)
* @see org.eclipse.jface.fieldassist.IContentProposalProvider#getProposals(java.lang.String, int)
*/
public synchronized IContentProposal[] getProposals(String contents, int position) {
String lastTagName = getLastWord(contents, position);
ITag[] matches = getMatchingTags(lastTagName.toLowerCase());
IContentProposal[] proposals = new IContentProposal[matches.length];
for (int i = 0; i < matches.length; i++) {
proposals[i] = new TagContentProposal(matches[i], lastTagName.length());
}
return proposals;
}
/**
* @return
*/
private ITag[] getMatchingTags(String name) {
if (tags == null) {
refreshTags();
}
if (name == null || "".equals(name)) return tags;
int closestIndex = findClosestMatch(name, 0, tags.length-1);
return tagsStaringWith(name, closestIndex);
}
/**
* @param name
* @param closestIndex
* @return
*/
private ITag[] tagsStaringWith(String name, int closestIndex) {
ArrayList<ITag> tagList = new ArrayList<ITag>();
for (int i = closestIndex ; i < tags.length; i++) {
String tagName = tags[i].getName().toLowerCase();
if (tagName.startsWith(name)) {
tagList.add(tags[i]);
} else {
break;
}
}
return tagList.toArray(new ITag[tagList.size()]);
}
/**
* Finds the lowest index of a tag matching the given lower-case name. Or, if
* a match cannot be found, it returns the next-lowest index.
* @param name
* @param i
* @param j
* @return
*/
private int findClosestMatch(String name, int i, int j) {
if (i >= j) {
if (!(tags[i].getName().toLowerCase().startsWith(name)))
i++;
return(iterateToLowest(i, name));
}
int index = i + ((j-i)/2);
String tagName = tags[index].getName().toLowerCase();
int diff = name.compareTo(tagName);
if (diff == 0) {
return iterateToLowest(index, name);
} else if (diff < 0) {
return findClosestMatch(name, i, index-1);
} else {
return findClosestMatch(name, index+1, j);
}
}
/**
* @param i
* @param name
* @return
*/
private int iterateToLowest(int i, String name) {
String tagName = tags[i].getName().toLowerCase();
if (!name.equals(tagName)) return i;
i--;
while (i >= 0) {
tagName = tags[i].getName().toLowerCase();
if (!name.equals(tagName)) return i+1;
i--;
}
return i+1;
}
/**
* @param contents
* @return
*/
private String getLastWord(String contents, int position) {
//scan backwards until we match a whitespace.
int i = position-1;
while ( i>0 && !Character.isWhitespace(contents.charAt(i)) )
i--;
if (i < 0) return "";
String s = contents.substring(i,position).trim().toLowerCase();
return s;
}
}