blob: fdfe5a7753eda71876793e1174b59941ff82d1ae [file] [log] [blame]
/******************************************************************************
* Copyright (c) 2002, 2007 IBM Corporation and others.
* 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:
* IBM Corporation - initial API and implementation
****************************************************************************/
package org.eclipse.gmf.runtime.common.ui.util;
import java.util.List;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.TreeEditor;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;
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.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.contentassist.ContentAssistHandler;
import org.eclipse.gmf.runtime.common.ui.contentassist.ContentAssistantHelper;
import org.eclipse.gmf.runtime.common.ui.internal.l10n.CommonUIMessages;
/**
* A class that enables inline-text editing for tree nodes
*
* @author Yasser Lulu
*
*/
public class TreeInlineTextEditor {
/**
* a tree editor used to aid in editing the nodes
*/
private TreeEditor treeEditor;
/**
* the tree whose nodes are being edited
*/
private Tree tree;
/**
* a text widget displayed to enable user string input
*/
private Text textEditor;
/**
* a composite parent for the text widget
*/
private Composite textEditorParent;
/**
* a zero size rectangle to force the text widget to disappear from screen
* when ending editing
*/
private static final Rectangle nullRectangle = new Rectangle(0, 0, 0, 0);
/**
* the final text entered and commited by the user
*/
private String finalText;
/**
* the initial string displayed when editing started
*/
private String initialText;
/**
* the edit string source and sink
*/
private IEditStringProvider editStringProvider;
/**
* the tree item currently being edited
*/
private TreeItem treeItem;
/**
* the viewer encapsulating the tree to edit
*/
private TreeViewer viewer;
/**
* the text action hanndler
*/
private IInlineTextActionHandler textActionHandler;
/**
* flag for disabling F2
*/
private boolean isF2disabled;
/**
* A semaphore that prevents re-entrance into the endEdit() behavior.
*/
private volatile boolean inEndEdit;
/**
* content assistant handler
*/
private ContentAssistHandler contentAssistHandler = null;
/**
* content assist background color
*/
private Color proposalPopupBackgroundColor;
/**
* content assist foreground color
*/
private Color proposalPopupForegroundColor;
/**
* Returns the isF2disabled.
*
* @return boolean
*/
private boolean isF2disabled() {
return isF2disabled;
}
/**
* Sets the isF2disabled.
*
* @param isF2disabled
* The isF2disabled to set
*/
private void setIsF2disabled(boolean isF2disabled) {
this.isF2disabled = isF2disabled;
}
/**
* returns the tree viewer
*
* @return TreeViewer
*/
private TreeViewer getTreeViewer() {
return viewer;
}
/**
* sets the tree viewer
*
* @param viewer
* the TreeViewer to set
*/
private void setTreeViewer(TreeViewer viewer) {
this.viewer = viewer;
}
/**
* return the currently edited tree item, null if none
*
* @return TreeItem the tree-item currently being edited or null if none
*/
private TreeItem getTreeItem() {
return treeItem;
}
/**
* returns the edit string provider
*
* @return IEditStringProvider theedit string provider
*/
private IEditStringProvider getEditStringProvider() {
return editStringProvider;
}
/**
* return the tree-editor
*
* @return TreeEditor the tree-editor
*/
private TreeEditor getTreeEditor() {
return treeEditor;
}
/**
* sets the tree-editor
*
* @param treeEditor
* The TreeEditor
*/
private void setTreeEditor(TreeEditor treeEditor) {
this.treeEditor = treeEditor;
}
/**
* Constructor for TreeInlineTextEditor.
*/
public TreeInlineTextEditor(TreeViewer treeViewer, IActionBars actionBars,
List disableActionsIds, IEditStringProvider editStringProvider) {
this(treeViewer, editStringProvider, false);
initTextActionHandler(actionBars, disableActionsIds);
}
/**
* Constructor for TreeInlineTextEditor.
*
* @param treeViewer the tree viewer
* @param editStringProvider
* @param isF2disabled boolean flag indicating whether F2 is disabled
*/
public TreeInlineTextEditor(TreeViewer treeViewer,
IEditStringProvider editStringProvider, boolean isF2disabled) {
setTreeViewer(treeViewer);
setTree(treeViewer.getTree());
setIsF2disabled(isF2disabled);
setEditStringProvider(editStringProvider);
createControl();
init();
}
/**
* answers whether this inline-editor has been disposed
*
* @return boolean indicating its dispose status
*/
public boolean isDisposed() {
return ((getTextEditorParent() == null) || (getTextEditorParent()
.isDisposed()));
}
/**
* answers if we can start editing
*
* @return boolean indicating if we can start editing
*/
public boolean canEdit() {
return ((isDisposed() == false) && (getTree().isDisposed() == false)
&& (getTree().getEnabled()) && (getTree().getVisible()) && (isSelectedItemEditable()));
}
/**
* Answers whether the currently selected tree-item meets all the editablity
* criteria
*
* @return boolean true if editable, false otherwise
*/
private boolean isSelectedItemEditable() {
return ((getTree().getSelection().length == 1)
&& (getTree().getSelection()[0].isDisposed() == false)
&& (getTree().getSelection()[0].getData() != null) && getEditStringProvider()
.canEdit(getTree().getSelection()[0].getData()));
}
/**
* starts the editing process
*/
public void startEdit() {
while (Display.getCurrent().readAndDispatch()) {
// process handler.setEnabled(false) queued in
// hide() before re-entering content assist mode
}
if (canEdit()) {
cancelEdit();
setTreeItem(getTree().getSelection()[0]);
setInitialText(getEditStringProvider().getEditString(
getTreeItem().getData()));
getTextEditor().setText(getInitialText());
getTreeEditor().setItem(getTreeItem());
show();
}
}
/**
* cancels the editing process
*/
public void cancelEdit() {
if (canProceed()) {
hide();
}
}
/**
* ends the editing process
*/
public void endEdit() {
if (inEndEdit) {
// prevent re-entrance into this method when we are already
// ending the edit. This prevents, in particular, the focus
// listener from ending the edit while we are ending it
return;
}
inEndEdit = true;
try {
if (canProceed()) {
setFinalText(getCurrentText());
if (getFinalText().equals(getInitialText()) == false) {
final Object obj = getTreeItem().getData();
getEditStringProvider().setEditString(
obj, getFinalText());
// Hide first so that the focus change when the dialog opens
// doesn't attempt to re-enter "endEdit()".
//RATLC00529737
//ask the label-provider to the update tree-node's label in order to
//display the newly entered name
getTreeItem().getDisplay().asyncExec(new Runnable() {
public void run() {
if (!isDisposed()) {
getTreeViewer().update(obj, null);
}
}
});
hide();
} else {
hide();
}
}
} finally {
inEndEdit = false;
}
}
/**
* Opens an error dialog for the specified status object.
*
* @param status
* The status object for which to open an error dialog.
*
*/
protected void openErrorDialog(IStatus status) {
ErrorDialog.openError(getShell(), CommonUIMessages.TreeInlineTextEditor_errorDialogTitle, null, status);
}
private Shell getShell() {
return getTree().getShell();
}
/**
* answers if it is ok to continue operation
*
* @return boolean indicating if it is ok to continue operation
*/
private boolean canProceed() {
return isDisposed() == false && getTreeItem() != null
&& !getTreeItem().isDisposed();
}
/**
* displays the text editing widget
*/
private void show() {
uninstallContentAssist(false);
if (getTreeItem() != null) {
IContentAssistProcessor processor = getEditStringProvider()
.getCompletionProcessor(getTreeItem().getData());
if (processor != null) {
// install content assist
contentAssistHandler = ContentAssistantHelper
.createTextContentAssistant(getTextEditor(),
proposalPopupForegroundColor,
proposalPopupBackgroundColor, processor);
}
}
getTextEditorParent().setEnabled(true);
getTextEditorParent().setVisible(true);
getTextEditor().setEnabled(true);
getTextEditor().setVisible(true);
adjustTextEditorBounds();
getTextEditorParent().redraw();
getTextEditor().selectAll();
getTextEditor().setFocus();
if (getTextActionHandler() != null) {
getTextActionHandler().hookHandlers();
}
}
/**
* hides the text editing widget
*/
private void hide() {
setTreeItem(null);
getTreeEditor().setItem(null);
getTextEditor().setVisible(false);
getTextEditor().setEnabled(false);
getTextEditorParent().setBounds(getNullRectangle());
getTextEditorParent().setVisible(false);
getTextEditorParent().setEnabled(false);
if (getTextActionHandler() != null) {
getTextActionHandler().unHookHandlers();
}
uninstallContentAssist(true);
}
/**
* Uninstalls content assist on the text widget, if installed
*
* @param fork
* whether to queue the uninstall or not
*/
private void uninstallContentAssist(boolean fork) {
if (contentAssistHandler != null) {
// uninstall content assist
final ContentAssistHandler localHandler = contentAssistHandler;
contentAssistHandler = null;
if (fork) {
Display.getCurrent().asyncExec(new Runnable() {
public void run() {
// Content Assist hack - queue disablement, otherwise
// cleanup on focus lost won't happen
localHandler.setEnabled(false);
}
});
} else {
localHandler.setEnabled(false);
}
}
}
/**
* Disposes the text widget and reset the editorText field.
* It becomes unreusable afterwards
*/
public void dispose() {
if (getTextEditorParent() != null) {
if (getTextActionHandler() != null) {
getTextActionHandler().dispose();
}
setTextActionHandler(null);
setTextEditorParent(null);
setTextEditor(null);
getTreeEditor().setEditor(null, null);
setTreeEditor(null);
setTree(null);
proposalPopupBackgroundColor.dispose();
proposalPopupForegroundColor.dispose();
}
}
/**
* creates the text widget and its parent composite
*/
private void createControl() {
setTextEditorParent(new Composite(getTree(), SWT.NONE));
setTreeEditor(new TreeEditor(getTree()));
getTreeEditor().horizontalAlignment = SWT.LEFT;
getTreeEditor().grabHorizontal = true;
getTreeEditor().setEditor(getTextEditorParent(), null);
getTextEditorParent().setVisible(false);
setTextEditor(new Text(getTextEditorParent(), SWT.NONE));
getTextEditorParent().setBackground(getTextEditor().getBackground());
proposalPopupBackgroundColor = new Color(getShell().getDisplay(),
new RGB(254, 241, 233));
proposalPopupForegroundColor = new Color(getShell().getDisplay(),
new RGB(0, 0, 0));
}
/**
* initializes the controls and listeners needed to manage the editing
* process
*/
private void init() {
getTextEditorParent().addListener(SWT.Paint, new Listener() {
public void handleEvent(Event e) {
Point textSize = getTextEditor().getSize();
Point parentSize = getTextEditorParent().getSize();
e.gc.drawRectangle(0, 0, Math.min(textSize.x + 4,
parentSize.x - 1), parentSize.y - 1);
}
});
getTextEditor().addListener(SWT.Modify, new Listener() {
public void handleEvent(Event e) {
adjustTextEditorBounds();
getTextEditorParent().redraw();
}
});
getTextEditor().addKeyListener(new KeyAdapter() {
public void keyReleased(KeyEvent event) {
if (event.character == SWT.CR) {
endEdit();
} else if (event.character == SWT.ESC) {
cancelEdit();
}
}
});
getTextEditor().addFocusListener(new FocusAdapter() {
public void focusGained(FocusEvent e) {
return;
}
public void focusLost(FocusEvent fe) {
Shell activeShell = fe.display.getActiveShell();
if (activeShell != null
&& getTextEditor().getShell().equals(
activeShell.getParent())) {
/*
* CONTENT ASSIST: focus is lost to the content assist pop
* up - stay in focus
*/
return;
}
if ((getTreeViewer().getSelection().isEmpty() == false)
&& canProceed()) {
final Object obj = getTreeItem().getData();
getTreeItem().getDisplay().asyncExec(new Runnable() {
public void run() {
if (!isDisposed()) {
getTreeViewer().update(obj, null);
}
}
});
}
endEdit();
}
});
getTreeViewer().addSelectionChangedListener(
new ISelectionChangedListener() {
public void selectionChanged(SelectionChangedEvent event) {
cancelEdit();
}
});
if (!isF2disabled()) {
getTree().addKeyListener(new KeyAdapter() {
public void keyReleased(KeyEvent event) {
if (event.keyCode == SWT.F2) {
startEdit();
}
}
});
}
}
/**
* initializes the text action handlers
*
* @param actionBars
* The IActionBars for the view-site to retarget global
* edit-cut-copy events for the text box
* @param disableActionsIds
* a List of global actions ids that are non-Eclipse and which
* we'll have to disable
*/
private void initTextActionHandler(IActionBars actionBars,
List disableActionsIds) {
if (actionBars == null) {
return;
}
actionBars.getMenuManager().addMenuListener(new IMenuListener() {
public void menuAboutToShow(IMenuManager manager) {
cancelEdit();
}
});
setTextActionHandler(new InlineTextActionHandler(actionBars,
getTextEditor(), disableActionsIds));
}
/**
* adjusts the bounds of the text widget
*/
private void adjustTextEditorBounds() {
Point textSize = getTextEditor().computeSize(SWT.DEFAULT, SWT.DEFAULT);
textSize.x += textSize.y; // increase width a little
Point parentSize = getTextEditorParent().getSize();
getTextEditor().setBounds(2, 1, Math.min(textSize.x, parentSize.x - 4),
parentSize.y - 2);
}
/**
* Get the Tree being edited.
*
* @returnTree
*/
private Tree getTree() {
return tree;
}
/**
* return the current text
*
* @return String the text currently in the text widget, or null if it is
* disposed already
*/
public String getCurrentText() {
return (canProceed()) ? getTextEditor().getText()
: null;
}
/**
* returns the initial value when editing started
*
* @return String the last initial value used when editing last started
*/
public String getInitialText() {
return initialText;
}
/**
* returns the comitted string by the user when editing ended (not-canclled)
*
* @return String the comitted string by the user
*/
public String getFinalText() {
return finalText;
}
/**
* returns the text widget
*
* @return Text the text widget used for editing
*/
private Text getTextEditor() {
return textEditor;
}
/**
* returns the text widget parent composite
*
* @return Composite text widget parent composite
*/
private Composite getTextEditorParent() {
return textEditorParent;
}
/**
* Sets the editStringProvider.
*
* @param editStringProvider
* The editStringProvider to set
*/
private void setEditStringProvider(IEditStringProvider editStringProvider) {
this.editStringProvider = editStringProvider;
}
/**
* Sets the textEditor.
*
* @param textEditor
* The textEditor to set
*/
private void setTextEditor(Text textEditor) {
this.textEditor = textEditor;
}
/**
* Sets the textEditorParent.
*
* @param textEditorParent
* The textEditorParent to set
*/
private void setTextEditorParent(Composite textEditorParent) {
this.textEditorParent = textEditorParent;
}
/**
* Sets the tree.
*
* @param tree
* The tree to set
*/
private void setTree(Tree tree) {
this.tree = tree;
}
/**
* Sets the treeItem.
*
* @param treeItem
* The treeItem to set
*/
private void setTreeItem(TreeItem treeItem) {
this.treeItem = treeItem;
}
/**
* Returns the nullRectangle.
*
* @return Rectangle
*/
private Rectangle getNullRectangle() {
return nullRectangle;
}
/**
* Sets the finalText.
*
* @param finalText
* The finalText to set
*/
private void setFinalText(String finalText) {
this.finalText = finalText;
}
/**
* Sets the initialText.
*
* @param initialText
* The initialText to set
*/
private void setInitialText(String initialText) {
this.initialText = initialText;
}
/**
* Returns the textActionHandler.
*
* @return TextActionHandler
*/
private IInlineTextActionHandler getTextActionHandler() {
return textActionHandler;
}
/**
* Sets the textActionHandler.
*
* @param textActionHandler
* The textActionHandler to set
*/
private void setTextActionHandler(IInlineTextActionHandler textActionHandler) {
this.textActionHandler = textActionHandler;
}
}