blob: e73b76d516ce100dc2a8f1cb67404f024ef1bf4b [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2014, 2018 QNX Software Systems and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Alena Laskavaia
*******************************************************************************/
package org.eclipse.launchbar.ui.controls.internal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.viewers.IFontProvider;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.launchbar.core.ILaunchDescriptor;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseTrackAdapter;
import org.eclipse.swt.events.MouseTrackListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Sash;
import org.eclipse.swt.widgets.Widget;
public class LaunchBarListViewer extends StructuredViewer {
private ScrolledComposite listScrolled;
private Composite listComposite;
private ListItem[] listItems;
private int selIndex;
private int itemH = 30;
private int scrollBucket;
private final int maxScrollBucket = 6;
private int separatorIndex = -1;
private boolean historySupported = true;
private ViewerComparator historyComparator;
private boolean finalSelection = false;
private FilterControl filterControl;
private Sash sash;
private String historyPref;
private static class LaunchBarListViewerComparator extends ViewerComparator {
public LaunchBarListViewerComparator(Comparator comp) {
super(comp);
}
// have to override it because standard ViewerComparator compares by labels only
@SuppressWarnings("unchecked")
@Override
public int compare(Viewer viewer, Object e1, Object e2) {
return ((Comparator) getComparator()).compare(e1, e2);
}
}
private TraverseListener listItemTraverseListener = e -> {
final ListItem currItem = selIndex >= 0 ? listItems[selIndex] : null;
if (currItem == null && e.keyCode != SWT.ARROW_DOWN) {
return;
}
if (e.detail == SWT.TRAVERSE_ARROW_NEXT || e.detail == SWT.TRAVERSE_TAB_NEXT) {
if (e.keyCode == SWT.ARROW_DOWN) {
int maxIdx = listItems.length - 1;
if (selIndex < maxIdx) {
// move to next item
listItems[selIndex + 1].setSelected(true);
if (scrollBucket < maxScrollBucket) {
scrollBucket++;
} else {
// need to scroll the list up 1 item
int sY1 = listScrolled.getOrigin().y;
listScrolled.setOrigin(0, sY1 + itemH);
}
} else if (selIndex == maxIdx && maxIdx > maxScrollBucket) {
// level the scroll for any offset at the bottom of the list
listScrolled.setOrigin(0, itemH * (maxIdx - maxScrollBucket + 1));
}
}
} else if (e.detail == SWT.TRAVERSE_ARROW_PREVIOUS || e.detail == SWT.TRAVERSE_TAB_PREVIOUS) {
if (e.keyCode == SWT.ARROW_UP) {
if (selIndex > 0) {
// move to previous item
if (scrollBucket > 0) {
scrollBucket--;
} else {
// need to scroll the list down 1 item
int sY2 = listScrolled.getOrigin().y;
listScrolled.setOrigin(0, sY2 - itemH);
}
listItems[selIndex - 1].setSelected(true);
} else if (selIndex == 0) {
// level any offset @ beginning
listScrolled.setOrigin(0, 0);
}
} else if (currItem.editButton != null) {
// remove focus from edit button
currItem.editButton.setSelected(false);
currItem.editButton.redraw();
}
} else if (e.detail == SWT.TRAVERSE_RETURN) {
setDefaultSelection(new StructuredSelection(currItem.element));
} else if (e.detail == SWT.TRAVERSE_ESCAPE) {
setDefaultSelection(new StructuredSelection());
}
};
private KeyListener lisItemKeyListener = new KeyListener() {
@Override
public void keyReleased(KeyEvent e) {
// ignore
}
@Override
public void keyPressed(KeyEvent e) {
if (e.character != 0 && e.character >= 0x20 && !filterControl.isVisible()) {
if (listItems.length <= 1)
return; // no filter for 1 item!
// enable filter control and send the character there
filterControl.setVisible(true);
filterControl.setFocus();
filterControl.getParent().layout(true);
filterControl.getFilterText().setText(e.character + ""); //$NON-NLS-1$
filterControl.getFilterText().setSelection(1);
} else if (e.character == SWT.ESC) {
setDefaultSelection(new StructuredSelection());
}
}
};
private class ListItem extends Composite {
protected final Object element;
private Label icon;
private Label label;
protected EditButton editButton;
private int index;
private ILabelProvider labelProvider;
@Override
public String toString() {
return "[" + index + "] " + labelProvider.getText(element); //$NON-NLS-1$ //$NON-NLS-2$
}
public ListItem(Composite parent, int style, Object element, int index, ILabelProvider labelProvider) {
super(parent, style);
this.element = element;
this.index = index;
this.labelProvider = labelProvider;
setData(element);
setBackground(parent.getBackground());
addPaintListener(new PaintListener() {
@Override
public void paintControl(PaintEvent e) {
removePaintListener(this);
if (label == null) {
lazyInit();
}
}
});
} // end ListItem(..)
protected void lazyInit() {
Image image = labelProvider.getImage(element);
int columns = 1;
if (image != null)
columns++;
GridLayout layout = new GridLayout(columns, false);
layout.marginWidth = layout.marginHeight = 7;
setLayout(layout);
MouseListener listItemMouseListener = new MouseAdapter() {
@Override
public void mouseUp(MouseEvent e) {
setDefaultSelection(new StructuredSelection(element));
}
};
MouseTrackListener listItemMouseTrackListener = new MouseTrackAdapter() {
@Override
public void mouseEnter(MouseEvent e) {
setSelected(true);
}
@Override
public void mouseExit(MouseEvent e) {
setSelected(false);
}
};
addMouseListener(listItemMouseListener);
addMouseTrackListener(listItemMouseTrackListener);
if (image != null) {
icon = createImage(this, image);
icon.addMouseListener(listItemMouseListener);
icon.addMouseTrackListener(listItemMouseTrackListener);
}
label = createLabel(this, element);
label.addMouseListener(listItemMouseListener);
label.addMouseTrackListener(listItemMouseTrackListener);
// add traverse listnener to control which will have keyboard focus
addTraverseListener(listItemTraverseListener);
addKeyListener(lisItemKeyListener);
layout(true);
}
public void setSelected(boolean selected) {
if (selected) {
setBackground(getDisplay().getSystemColor(SWT.COLOR_LIST_SELECTION));
int idx = getIndex();
if (idx != selIndex) {
if (selIndex >= 0) {
listItems[selIndex].setBackground(getParent().getBackground());
scrollBucket = Math.max(Math.min(scrollBucket + idx - selIndex, maxScrollBucket), 0);
} else { // initially
scrollBucket = Math.min(idx, maxScrollBucket);
}
}
selIndex = idx;
} else {
setBackground(getParent().getBackground());
}
if (editButton != null) {
editButton.setSelected(selected);
}
}
@Override
public void setBackground(Color color) {
super.setBackground(color);
if (icon != null && !icon.isDisposed())
icon.setBackground(color);
if (label != null && !label.isDisposed())
label.setBackground(color);
if (editButton != null && !editButton.isDisposed())
editButton.setBackground(color);
}
public void setImage(Image image) {
if (icon != null && !icon.isDisposed())
icon.setImage(image);
}
public void setText(String text) {
if (!label.isDisposed())
label.setText(text);
}
@Override
public boolean setFocus() {
super.setFocus();
return true;
}
protected int getIndex() {
return index;
}
private Label createImage(Composite parent, Image image) {
Rectangle bounds = image.getBounds();
boolean disposeImage = false;
if (bounds.height > 16 || bounds.width > 16) {
Image buttonImage = new Image(getDisplay(), 16, 16);
GC gc = new GC(buttonImage);
gc.setAntialias(SWT.ON);
gc.setInterpolation(SWT.HIGH);
// resize to 16 pixels
gc.drawImage(image, 0, 0, image.getBounds().width, image.getBounds().height, 0, 0, 16, 16);
gc.dispose();
image = buttonImage;
disposeImage = true;
}
Label icon = new Label(parent, SWT.NONE);
icon.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, true));
icon.setImage(image);
if (disposeImage) {
final Image disposableImage = image;
icon.addDisposeListener(e -> disposableImage.dispose());
}
icon.setBackground(parent.getBackground());
return icon;
}
private Label createLabel(Composite parent, Object element) {
Label label = new Label(parent, SWT.NONE);
label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, true));
ILabelProvider labelProvider = (ILabelProvider) getLabelProvider();
label.setText(labelProvider.getText(element));
if (labelProvider instanceof IFontProvider) {
label.setFont(((IFontProvider) labelProvider).getFont(element));
}
label.setBackground(parent.getBackground());
return label;
}
} // end ListItem class
public LaunchBarListViewer(Composite parent) {
filterControl = new FilterControl(parent);
filterControl.setBackground(parent.getBackground());
listScrolled = new ScrolledComposite(parent, SWT.V_SCROLL | SWT.NO_BACKGROUND);
listScrolled.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
listScrolled.setExpandHorizontal(true);
listScrolled.setBackground(parent.getBackground());
listComposite = new Composite(listScrolled, SWT.NONE);
listComposite.setBackground(parent.getBackground());
listScrolled.setContent(listComposite);
listComposite.setLayout(GridLayoutFactory.fillDefaults().spacing(0, 0).create());
selIndex = -1;
scrollBucket = 0;
filterControl.attachListViewer(this);
historySupported = false;
setHistoryPreferenceName(getHistoryPreferenceName());
}
private void createSash(final Composite listComp) {
if (separatorIndex < 0 || !historySupported)
return;
sash = new Sash(listComp, SWT.BORDER | SWT.HORIZONTAL);
sash.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
if (separatorIndex < listItems.length)
sash.moveAbove(listItems[separatorIndex]);
else
sash.moveBelow(null);
sash.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_LIST_SELECTION));
sash.addListener(SWT.Selection, e -> separatorIndex = (e.y + itemH / 2) / itemH);
sash.addMouseListener(new MouseListener() {
@Override
public void mouseUp(MouseEvent e) {
setSeparatorIndex(separatorIndex); // call setter if it was overriden
if (separatorIndex >= 0) {
if (separatorIndex < listItems.length)
sash.moveAbove(listItems[separatorIndex]);
else
sash.moveBelow(null);
listComp.layout();
}
}
@Override
public void mouseDown(MouseEvent e) {
if (!Platform.getOS().equals(Platform.OS_MACOSX)) { // does not work on mac
sash.moveAbove(null); // keep on top so user see it when moving
}
}
@Override
public void mouseDoubleClick(MouseEvent e) {
// ignore
}
});
sash.setToolTipText(Messages.LaunchBarListViewer_0);
}
@Override
public Control getControl() {
return listScrolled;
}
@Override
protected void inputChanged(Object input, Object oldInput) {
super.inputChanged(input, oldInput);
refreshAll();
}
protected void refreshAll() {
selIndex = -1;
Control[] children = listComposite.getChildren();
for (Control control : children) {
control.dispose();
}
Object[] origElements = getElements();
Object[] elements = filterElements(origElements);
listItems = new ListItem[elements.length];
if (elements.length > 0) {
listItems[0] = createListItem(elements, 0);
itemH = Math.max(listItems[0].computeSize(SWT.DEFAULT, SWT.DEFAULT).y, 16);
for (int i = 1; i < elements.length; ++i) {
listItems[i] = createListItem(elements, i);
}
createSash(listComposite);
}
listComposite.pack(true);
listComposite.layout(true, true);
GridData gd = new GridData(SWT.FILL, SWT.FILL, true, true);
if (elements.length > maxScrollBucket) {
Rectangle bounds = listItems[maxScrollBucket].getBounds();
gd.heightHint = Math.max(bounds.y + bounds.height, itemH * (maxScrollBucket + 1));
}
listScrolled.setLayoutData(gd);
listScrolled.layout(true);
}
private ListItem createListItem(Object[] elements, int i) {
ListItem item = new ListItem(listComposite, SWT.NONE, elements[i], i, (ILabelProvider) getLabelProvider());
GridData gd = new GridData(SWT.FILL, SWT.FILL, true, false);
item.setLayoutData(gd);
if (i <= maxScrollBucket) { // this is how many visible by default
item.lazyInit();
} else {
gd.heightHint = itemH;
}
return item;
}
@Override
protected Widget doFindInputItem(Object element) {
return doFindItem(element);
}
@Override
protected Widget doFindItem(Object element) {
if (listItems == null)
return null;
for (ListItem listItem : listItems) {
if (listItem.element.equals(element))
return listItem;
}
return null;
}
@Override
protected void doUpdateItem(Widget item, Object element, boolean fullMap) {
if (item instanceof ListItem) {
((ListItem) item).lazyInit();
}
}
@SuppressWarnings("rawtypes")
@Override
protected List getSelectionFromWidget() {
ArrayList<Object> arrayList = new ArrayList<>();
if (selIndex >= 0)
arrayList.add(listItems[selIndex].element);
return arrayList;
}
@Override
protected void internalRefresh(Object element) {
if (element == null || element == getRoot()) {
refreshAll();
return;
}
ListItem item = (ListItem) doFindItem(element);
ILabelProvider lp = (ILabelProvider) getLabelProvider();
if (lp == null || item == null)
return;
item.setImage(lp.getImage(element));
item.setText(lp.getText(element));
}
private Object[] filterElements(Object[] elements) {
Object[] topElements = elements.clone();
if (getComparator() != null)
getComparator().sort(this, elements);
if (getTopComparator() != null)
getTopComparator().sort(this, topElements);
// only bottom part will be filtered
Object[] result = elements;
if (getFilters() != null) {
for (ViewerFilter f : getFilters()) {
result = f.filter(this, (Object) null, result);
}
}
if (separatorIndex <= 0 || !historySupported)
return result;
if (separatorIndex >= topElements.length) {
return topElements; // all elements will fit in top elements
}
ILaunchDescriptor[] descsCopy = new ILaunchDescriptor[separatorIndex + result.length];
System.arraycopy(topElements, 0, descsCopy, 0, separatorIndex); // copy first N elements
System.arraycopy(result, 0, descsCopy, separatorIndex, result.length); // copy all into rest
return descsCopy;
}
private Object[] getElements() {
IStructuredContentProvider cp = (IStructuredContentProvider) getContentProvider();
if (cp == null)
return new Object[0];
Object[] elements = cp.getElements(getInput());
return elements;
}
@Override
public void reveal(Object element) {
// TODO Auto-generated method stub
}
public void setDefaultSelection(StructuredSelection selection) {
finalSelection = true;
setSelection(selection, true);
}
@Override
protected void setSelectionToWidget(@SuppressWarnings("rawtypes") List l, boolean reveal) {
if (l.size() == 0) {
return;
}
Object sel = l.get(0);
Widget wid = doFindItem(sel);
if (wid instanceof ListItem) {
ListItem listItem = (ListItem) wid;
listItem.setSelected(true);
}
}
public int getSeparatorIndex() {
return separatorIndex;
}
public void setSeparatorIndex(int separatorIndex) {
this.separatorIndex = separatorIndex;
if (separatorIndex <= 0)
return;
IPreferenceStore store = Activator.getDefault().getPreferenceStore();
String prefName = getHistoryPreferenceName();
if (prefName != null && store.getInt(prefName) != getSeparatorIndex()) {
store.setValue(prefName, getSeparatorIndex());
}
}
protected String getHistoryPreferenceName() {
return historyPref;
}
public int getItemCount() {
return listItems.length;
}
/**
* Returns top element (provider element) in the begging on non-history list
*
* @return
*/
public Object getTopFilteredElement() {
if (listItems.length > 0) {
if (separatorIndex <= 0 || separatorIndex >= listItems.length || !historySupported)
return listItems[0].element;
else
return listItems[separatorIndex].element;
}
return null;
}
public Object getTopElement() {
if (listItems.length > 0) {
return listItems[0].element;
}
return null;
}
public ViewerComparator getTopComparator() {
return historyComparator;
}
/**
* ViewerComparator comparator labels of elements by default
*
* @param comp
*/
public void setHistoryComparator(ViewerComparator comp) {
historyComparator = comp;
}
public void setHistoryComparator(Comparator<?> comp) {
historyComparator = comp == null ? null : new LaunchBarListViewerComparator(comp);
}
public void setComparator(Comparator<?> comp) {
setComparator(comp == null ? null : new LaunchBarListViewerComparator(comp));
}
public boolean isHistorySupported() {
return historySupported;
}
public void setHistorySupported(boolean historySupported) {
this.historySupported = historySupported;
}
public void setHistoryPreferenceName(String historyPreferenceName) {
this.historyPref = historyPreferenceName;
if (historyPreferenceName != null) {
IPreferenceStore store = Activator.getDefault().getPreferenceStore();
int separator = store.getInt(historyPreferenceName);
if (separator <= 0)
separator = 1;
setSeparatorIndex(separator);
}
}
/**
* final selection will be set to true when user made a final selection in a list for example when double click on entry or
* pressed enter key
*/
public boolean isFinalSelection() {
return finalSelection;
}
public void setFinalSelection(boolean finalSelection) {
this.finalSelection = finalSelection;
}
public void setFocus() {
if (selIndex >= 0 && listItems != null && listItems.length < selIndex)
listItems[selIndex].setFocus();
else
getControl().setFocus();
}
public void setFilterVisible(boolean vis) {
filterControl.setVisible(vis);
}
}