/*******************************************************************************
* Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
* Copyright (C) 2008, Roger C. Soares <rogersoares@intelinet.com.br>
* Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
* Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com>
* Copyright (C) 2013, Robin Stocker <robin@nibor.org>
* Copyright (C) 2016, Thomas Wolf <thomas.wolf@paranor.ch>
*
* 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
*******************************************************************************/
package org.eclipse.egit.ui.internal.history;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.eclipse.core.runtime.jobs.IJobChangeEvent;
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
import org.eclipse.egit.ui.Activator;
import org.eclipse.egit.ui.UIPreferences;
import org.eclipse.egit.ui.internal.UIIcons;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.ToolBarManager;
import org.eclipse.jface.preference.IPersistentPreferenceStore;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.resource.ResourceManager;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevFlag;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
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.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
/**
* A toolbar for the history page.
*
* @see FindToolbarJob
* @see FindResults
* @see GitHistoryPage
*/
public class FindToolbar extends Composite {
/**
* Interface to receive status messages from the {@link FindToolbar}. The
* toolbar produces messages indicating a search result overflow, or the
* number of hits, or, when navigation among search results occurs, which
* entry is the current one.
*/
public interface StatusListener {
/**
* Invoked whenever the {@link FindToolbar} produces a new message. The
* message may be empty.
*
* @param originator
* of the message
* @param text
* of the message
*/
public void setMessage(FindToolbar originator, String text);
}
/**
* Preference value for searching all the fields
*/
public static final int PREFS_FINDIN_ALL = 0;
private static final int PREFS_FINDIN_COMMENTS = 1;
private static final int PREFS_FINDIN_AUTHOR = 2;
private static final int PREFS_FINDIN_COMMITID = 3;
private static final int PREFS_FINDIN_COMMITTER = 4;
private static final int PREFS_FINDIN_REFERENCE = 5;
private Color errorBackgroundColor;
/**
* The results (matches) of the current find operation.
*/
private final FindResults findResults;
private IFindListener listener;
private IPersistentPreferenceStore store = (IPersistentPreferenceStore) Activator.getDefault().getPreferenceStore();
private List<Listener> eventList = new ArrayList<>();
private Table historyTable;
private SWTCommit[] fileRevisions;
private Text patternField;
private ModifyListener patternModifyListener;
private Action findNextAction;
private Action findPreviousAction;
private String lastErrorPattern;
private ToolItem prefsDropDown;
private Menu prefsMenu;
private MenuItem caseItem;
private MenuItem allItem;
private MenuItem commitIdItem;
private MenuItem commentsItem;
private MenuItem authorItem;
private MenuItem committerItem;
private MenuItem referenceItem;
private Image allIcon;
private Image commitIdIcon;
private Image commentsIcon;
private Image authorIcon;
private Image committerIcon;
private Image branchesIcon;
private FindToolbarJob job;
private int currentPosition = -1;
/**
* Id of a commit that shall be moved to initially if it is part of the
* search results. If not set, or there is no such commit in the search
* results, the first search result will be revealed.
*/
private ObjectId preselect;
private CopyOnWriteArrayList<StatusListener> layoutListeners = new CopyOnWriteArrayList<>();
/**
* Creates the toolbar.
*
* @param parent
* the parent widget
*/
public FindToolbar(Composite parent) {
super(parent, SWT.NULL);
findResults = new FindResults();
listener = createFindListener();
findResults.addFindListener(listener);
setBackground(null);
createToolbar();
}
private void createToolbar() {
errorBackgroundColor = new Color(getDisplay(), new RGB(255, 150, 150));
ResourceManager resourceManager = Activator.getDefault()
.getResourceManager();
allIcon = UIIcons.getImage(resourceManager, UIIcons.SEARCH_COMMIT);
commitIdIcon = UIIcons.getImage(resourceManager,
UIIcons.ELCL16_ID);
commentsIcon = UIIcons.getImage(resourceManager,
UIIcons.ELCL16_COMMENTS);
authorIcon = UIIcons.getImage(resourceManager, UIIcons.ELCL16_AUTHOR);
committerIcon = UIIcons.getImage(resourceManager,
UIIcons.ELCL16_COMMITTER);
branchesIcon = UIIcons.getImage(resourceManager, UIIcons.BRANCHES);
GridLayout findLayout = new GridLayout();
findLayout.marginHeight = 0;
findLayout.marginBottom = 1;
findLayout.marginWidth = 0;
findLayout.numColumns = 5;
setLayout(findLayout);
setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
patternField = new Text(this,
SWT.SEARCH | SWT.ICON_CANCEL | SWT.ICON_SEARCH);
GridData findTextData = new GridData(SWT.FILL, SWT.LEFT, true, false);
findTextData.minimumWidth = 150;
patternField.setLayoutData(findTextData);
patternField.setMessage(UIText.HistoryPage_findbar_find_msg);
patternField.setTextLimit(100);
patternField.setFont(JFaceResources.getDialogFont());
ToolBarManager manager = new ToolBarManager(SWT.HORIZONTAL);
findNextAction = new Action() {
@Override
public void run() {
findNext();
}
};
findNextAction.setImageDescriptor(UIIcons.ELCL16_NEXT);
findNextAction.setText(UIText.HistoryPage_findbar_next);
findNextAction.setToolTipText(UIText.FindToolbar_NextTooltip);
findNextAction.setEnabled(false);
manager.add(findNextAction);
findPreviousAction = new Action() {
@Override
public void run() {
findPrevious();
}
};
findPreviousAction.setImageDescriptor(UIIcons.ELCL16_PREVIOUS);
findPreviousAction.setText(UIText.HistoryPage_findbar_previous);
findPreviousAction.setToolTipText(UIText.FindToolbar_PreviousTooltip);
findPreviousAction.setEnabled(false);
manager.add(findPreviousAction);
final ToolBar toolBar = manager.createControl(this);
prefsDropDown = new ToolItem(toolBar, SWT.DROP_DOWN);
prefsMenu = new Menu(getShell(), SWT.POP_UP);
caseItem = new MenuItem(prefsMenu, SWT.CHECK);
caseItem.setText(UIText.HistoryPage_findbar_ignorecase);
new MenuItem(prefsMenu, SWT.SEPARATOR);
allItem = createFindInMenuItem();
allItem.setText(UIText.HistoryPage_findbar_all);
allItem.setImage(allIcon);
commentsItem = createFindInMenuItem();
commentsItem.setText(UIText.HistoryPage_findbar_comments);
commentsItem.setImage(commentsIcon);
authorItem = createFindInMenuItem();
authorItem.setText(UIText.HistoryPage_findbar_author);
authorItem.setImage(authorIcon);
commitIdItem = createFindInMenuItem();
commitIdItem.setText(UIText.HistoryPage_findbar_commit);
commitIdItem.setImage(commitIdIcon);
committerItem = createFindInMenuItem();
committerItem.setText(UIText.HistoryPage_findbar_committer);
committerItem.setImage(committerIcon);
referenceItem = createFindInMenuItem();
referenceItem.setText(UIText.HistoryPage_findbar_reference);
referenceItem.setImage(branchesIcon);
prefsDropDown.addListener(SWT.Selection, new Listener() {
@Override
public void handleEvent(Event event) {
if (event.detail == SWT.ARROW) {
// Arrow clicked, show drop down menu
Rectangle itemBounds = prefsDropDown.getBounds();
Point point = toolBar.toDisplay(itemBounds.x, itemBounds.y
+ itemBounds.height);
prefsMenu.setLocation(point);
prefsMenu.setVisible(true);
} else {
// Button clicked, cycle to next option
if (allItem.getSelection())
selectFindInItem(commentsItem);
else if (commentsItem.getSelection())
selectFindInItem(authorItem);
else if (authorItem.getSelection())
selectFindInItem(commitIdItem);
else if (commitIdItem.getSelection())
selectFindInItem(committerItem);
else if (committerItem.getSelection())
selectFindInItem(referenceItem);
else if (referenceItem.getSelection())
selectFindInItem(allItem);
}
}
});
patternModifyListener = new ModifyListener() {
@Override
public void modifyText(ModifyEvent e) {
final FindToolbarJob finder = createFinder();
finder.setUser(false);
finder.schedule(200);
}
};
patternField.addModifyListener(patternModifyListener);
patternField.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetDefaultSelected(SelectionEvent e) {
if (e.detail != SWT.ICON_CANCEL
&& !patternField.getText().isEmpty()) {
// ENTER or the search icon clicked
final FindToolbarJob finder = createFinder();
finder.setUser(false);
finder.schedule();
}
}
});
patternField.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.keyCode == SWT.ARROW_DOWN) {
findNext();
} else if (e.keyCode == SWT.ARROW_UP) {
findPrevious();
}
}
});
caseItem.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
store.setValue(UIPreferences.FINDTOOLBAR_IGNORE_CASE,
caseItem.getSelection());
if (store.needsSaving()){
try {
store.save();
} catch (IOException e1) {
Activator.handleError(e1.getMessage(), e1, false);
}
}
clear();
}
});
caseItem.setSelection(store
.getBoolean(UIPreferences.FINDTOOLBAR_IGNORE_CASE));
int selectedPrefsItem = store.getInt(UIPreferences.FINDTOOLBAR_FIND_IN);
if (selectedPrefsItem == PREFS_FINDIN_ALL)
selectFindInItem(allItem);
else if (selectedPrefsItem == PREFS_FINDIN_COMMENTS)
selectFindInItem(commentsItem);
else if (selectedPrefsItem == PREFS_FINDIN_AUTHOR)
selectFindInItem(authorItem);
else if (selectedPrefsItem == PREFS_FINDIN_COMMITID)
selectFindInItem(commitIdItem);
else if (selectedPrefsItem == PREFS_FINDIN_COMMITTER)
selectFindInItem(committerItem);
else if (selectedPrefsItem == PREFS_FINDIN_REFERENCE)
selectFindInItem(referenceItem);
registerDisposal();
}
private void registerDisposal() {
addDisposeListener(new DisposeListener() {
@Override
public void widgetDisposed(DisposeEvent e) {
findResults.removeFindListener(listener);
findResults.clear();
listener = null;
if (job != null) {
job.cancel();
job = null;
}
prefsMenu.dispose();
errorBackgroundColor.dispose();
if (historyTable != null && !historyTable.isDisposed()) {
historyTable.clearAll();
}
}
});
}
/**
* Defines the commit to be set initially. If {@code null} or the search
* results do not contain such a commit, the first search result will be
* revealed.
*
* @param commitId
* to reveal
*/
public void setPreselect(ObjectId commitId) {
preselect = commitId;
}
@Override
public boolean setFocus() {
return patternField.setFocus();
}
/**
* Sets the text of the widget's search text field, and optionally triggers
* a search.
*
* @param text
* to set
* @param search
* if {@code true}, triggers a search after having set the text
*/
public void setText(String text, boolean search) {
if (!search) {
patternField.removeModifyListener(patternModifyListener);
}
patternField.setText(text);
if (!search) {
patternField.addModifyListener(patternModifyListener);
}
}
/**
* Sets the text of the widget's search text field without triggering a
* search.
*
* @param text
* to set
*/
public void setText(String text) {
setText(text, false);
}
/**
* Retrieves the current text in the widget's search text field.
*
* @return the text
*/
public String getText() {
return patternField.getText();
}
@Override
public void addListener(int evtType, Listener mouseListener) {
patternField.addListener(evtType, mouseListener);
}
@Override
public void removeListener(int evtType, Listener mouseListener) {
patternField.removeListener(evtType, mouseListener);
}
@Override
public void addKeyListener(KeyListener keyListener) {
patternField.addKeyListener(keyListener);
}
@Override
public void removeKeyListener(KeyListener keyListener) {
patternField.removeKeyListener(keyListener);
}
/**
* Adds the given listener to the widget, if it hasn't been added yet.
*
* @param layoutListener
* to add
*/
public void addStatusListener(StatusListener layoutListener) {
layoutListeners.addIfAbsent(layoutListener);
}
/**
* Removes the given listener if it had been added.
*
* @param layoutListener
* to remove
*/
public void removeStatusListener(StatusListener layoutListener) {
layoutListeners.remove(layoutListener);
}
private void notifyStatus(String text) {
for (StatusListener l : layoutListeners) {
l.setMessage(this, text);
}
}
private void findNext() {
find(true);
}
private void findPrevious() {
find(false);
}
private void find(boolean next) {
if (patternField.getText().length() > 0 && findResults.size() == 0) {
// If the toolbar was cleared and has a pattern typed,
// then we redo the find with the new table data.
final FindToolbarJob finder = createFinder();
finder.setUser(false);
finder.schedule();
patternField.setSelection(0, 0);
} else {
int currentIx = historyTable.getSelectionIndex();
int newIx = -1;
if (next) {
newIx = findResults.getIndexAfter(currentIx);
if (newIx == -1) {
newIx = findResults.getFirstIndex();
}
} else {
newIx = findResults.getIndexBefore(currentIx);
if (newIx == -1) {
newIx = findResults.getLastIndex();
}
}
notifyListeners(newIx);
String current = null;
currentPosition = findResults.getMatchNumberFor(newIx);
if (currentPosition == -1) {
current = "-"; //$NON-NLS-1$
} else {
current = String.valueOf(currentPosition);
}
notifyStatus(current + '/' + findResults.size());
}
}
private MenuItem createFindInMenuItem() {
final MenuItem menuItem = new MenuItem(prefsMenu, SWT.RADIO);
menuItem.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
selectFindInItem(menuItem);
}
});
return menuItem;
}
private void selectFindInItem(final MenuItem menuItem) {
if (menuItem == allItem)
selectFindInItem(menuItem, PREFS_FINDIN_ALL, allIcon,
UIText.HistoryPage_findbar_changeto_comments);
else if (menuItem == commentsItem)
selectFindInItem(menuItem, PREFS_FINDIN_COMMENTS, commentsIcon,
UIText.HistoryPage_findbar_changeto_author);
else if (menuItem == authorItem)
selectFindInItem(menuItem, PREFS_FINDIN_AUTHOR, authorIcon,
UIText.HistoryPage_findbar_changeto_commit);
else if (menuItem == commitIdItem)
selectFindInItem(menuItem, PREFS_FINDIN_COMMITID, commitIdIcon,
UIText.HistoryPage_findbar_changeto_committer);
else if (menuItem == committerItem)
selectFindInItem(menuItem, PREFS_FINDIN_COMMITTER, committerIcon,
UIText.HistoryPage_findbar_changeto_reference);
else if (menuItem == referenceItem)
selectFindInItem(menuItem, PREFS_FINDIN_REFERENCE, branchesIcon,
UIText.HistoryPage_findbar_changeto_all);
}
private void selectFindInItem(MenuItem menuItem, int preferenceValue,
Image dropDownIcon, String dropDownToolTip) {
prefsDropDown.setImage(dropDownIcon);
prefsDropDown.setToolTipText(dropDownToolTip);
findInPreferenceChanged(preferenceValue, menuItem);
}
private void findInPreferenceChanged(int findin, MenuItem item) {
store.setValue(UIPreferences.FINDTOOLBAR_FIND_IN, findin);
if (store.needsSaving()){
try {
store.save();
} catch (IOException e) {
Activator.handleError(e.getMessage(), e, false);
}
}
allItem.setSelection(false);
commitIdItem.setSelection(false);
commentsItem.setSelection(false);
authorItem.setSelection(false);
committerItem.setSelection(false);
referenceItem.setSelection(false);
item.setSelection(true);
clear();
}
private FindToolbarJob createFinder() {
if (job != null) {
job.cancel();
}
final String currentPattern = patternField.getText();
job = new FindToolbarJob(MessageFormat
.format(UIText.HistoryPage_findbar_find, currentPattern),
findResults);
job.pattern = currentPattern;
job.fileRevisions = fileRevisions;
job.ignoreCase = caseItem.getSelection();
if (allItem.getSelection()) {
job.findInCommitId = true;
job.findInComments = true;
job.findInAuthor = true;
job.findInCommitter = true;
job.findInReference = true;
} else {
job.findInCommitId = commitIdItem.getSelection();
job.findInComments = commentsItem.getSelection();
job.findInAuthor = authorItem.getSelection();
job.findInCommitter = committerItem.getSelection();
job.findInReference = referenceItem.getSelection();
}
job.addJobChangeListener(new JobChangeAdapter() {
private final FindToolbarJob myJob = job;
@Override
public void done(final IJobChangeEvent event) {
if (event.getResult().isOK()) {
getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
if (myJob != job
|| myJob.fileRevisions != fileRevisions) {
// Job superseded by another one; or input
// changed
return;
}
if (!isDisposed()) {
findCompletionUpdate(currentPattern,
findResults.isOverflow());
}
}
});
}
}
});
return job;
}
/**
* Sets the table that will have its selected items changed by this toolbar.
* Sets the list to be searched.
*
* @param hFlag
* @param historyTable
* @param commitArray
*/
void setInput(final RevFlag hFlag, final Table historyTable,
final SWTCommit[] commitArray) {
// this may cause a FindBugs warning, but
// copying the array is probably not a good
// idea
if (job != null) {
job.cancel();
}
this.fileRevisions = commitArray;
this.historyTable = historyTable;
findResults.setHighlightFlag(hFlag);
}
private void findCompletionUpdate(String pattern, boolean overflow) {
int total = findResults.size();
String label;
if (total > 0) {
String position = (currentPosition < 0) ? "1" //$NON-NLS-1$
: Integer.toString(currentPosition);
if (overflow) {
label = UIText.HistoryPage_findbar_exceeded + ' ' + position
+ '/' + total;
} else {
label = position + '/' + total;
}
if (currentPosition < 0) {
currentPosition = 1;
int ix = findResults.getFirstIndex();
notifyListeners(ix);
}
patternField.setBackground(null);
findNextAction.setEnabled(total > 1);
findPreviousAction.setEnabled(total > 1);
lastErrorPattern = null;
} else {
currentPosition = -1;
if (pattern.length() > 0) {
patternField.setBackground(errorBackgroundColor);
label = UIText.HistoryPage_findbar_notFound;
// Don't keep beeping every time if the user is deleting
// a long not found pattern
if (lastErrorPattern == null
|| !lastErrorPattern.startsWith(pattern)) {
getDisplay().beep();
findNextAction.setEnabled(false);
findPreviousAction.setEnabled(false);
}
lastErrorPattern = pattern;
} else {
patternField.setBackground(null);
label = ""; //$NON-NLS-1$
findNextAction.setEnabled(false);
findPreviousAction.setEnabled(false);
lastErrorPattern = null;
}
}
historyTable.clearAll();
if (overflow) {
Display display = getDisplay();
display.beep();
}
notifyStatus(label);
}
/**
* Clears the toolbar.
*/
void clear() {
if (!isDisposed()) {
patternField.setBackground(null);
if (patternField.getText().length() > 0) {
patternField.selectAll();
}
}
lastErrorPattern = null;
if (job != null) {
job.cancel();
job = null;
}
findResults.clear();
}
private void notifyListeners(int index) {
if (index >= 0) {
Event event = new Event();
event.type = SWT.Selection;
event.index = index;
event.widget = this;
event.data = fileRevisions[index];
for (Listener toNotify : eventList) {
toNotify.handleEvent(event);
}
}
}
/**
* Adds a selection event listener. The toolbar generates events when it
* selects an item in the history table
*
* @param selectionListener
* the listener that will receive the event
*/
public void addSelectionListener(Listener selectionListener) {
eventList.add(selectionListener);
}
/**
* Removes a selection listener if it had been added.
*
* @param selectionListener
* to remove
*/
public void removeSelectionListener(Listener selectionListener) {
eventList.remove(selectionListener);
}
private IFindListener createFindListener() {
return new IFindListener() {
private static final long UPDATE_INTERVAL = 200L; // ms
private long lastUpdate = 0L;
@Override
public void itemAdded(final int index, RevObject rev) {
long now = System.currentTimeMillis();
if (preselect != null && preselect.equals(rev.getId())
|| preselect == null && currentPosition < 0) {
currentPosition = findResults.getMatchNumberFor(index);
preselect = null;
getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
notifyListeners(index);
}
});
}
if (now - lastUpdate > UPDATE_INTERVAL && !isDisposed()) {
final boolean firstUpdate = lastUpdate == 0L;
lastUpdate = now;
getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
if (isDisposed()) {
return;
}
int total = findResults.size();
if (total > 0) {
String label;
if (currentPosition == -1) {
label = "-/" + total; //$NON-NLS-1$
} else {
label = Integer.toString(currentPosition)
+ '/' + total;
}
findNextAction.setEnabled(total > 1);
findPreviousAction.setEnabled(total > 1);
patternField.setBackground(null);
if (firstUpdate) {
historyTable.clearAll();
}
notifyStatus(label);
} else {
clear();
}
}
});
}
}
@Override
public void cleared() {
lastUpdate = 0L;
if (Display.getCurrent() == null) {
getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
clear();
}
});
} else {
clear();
}
}
private void clear() {
currentPosition = -1;
if (!isDisposed()) {
findNextAction.setEnabled(false);
findPreviousAction.setEnabled(false);
notifyStatus(""); //$NON-NLS-1$
}
if (historyTable != null && !historyTable.isDisposed()) {
historyTable.clearAll();
}
}
};
}
}