org.eclipse.egit.ui.internal.dialogs.SpellcheckableMessageArea.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.egit.ui.internal.dialogs.SpellcheckableMessageArea.java

Source

/*******************************************************************************
 * Copyright (C) 2010, Benjamin Muskalla <bmuskalla@eclipsesource.com>
 *
 * 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:
 *    Benjamin Muskalla (EclipseSource) - initial implementation
 *******************************************************************************/
package org.eclipse.egit.ui.internal.dialogs;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.eclipse.egit.ui.Activator;
import org.eclipse.egit.ui.UIPreferences;
import org.eclipse.egit.ui.UIText;
import org.eclipse.egit.ui.UIUtils;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.action.SubMenuManager;
import org.eclipse.jface.commands.ActionHandler;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.ITextOperationTarget;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.MarginPainter;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.quickassist.IQuickAssistInvocationContext;
import org.eclipse.jface.text.quickassist.IQuickAssistProcessor;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.AnnotationModel;
import org.eclipse.jface.text.source.IAnnotationAccess;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.text.source.ISharedTextColors;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.SourceViewer;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.ActiveShellExpression;
import org.eclipse.ui.IWorkbenchCommandConstants;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.editors.text.EditorsUI;
import org.eclipse.ui.editors.text.TextSourceViewerConfiguration;
import org.eclipse.ui.handlers.IHandlerActivation;
import org.eclipse.ui.handlers.IHandlerService;
import org.eclipse.ui.texteditor.AnnotationPreference;
import org.eclipse.ui.texteditor.DefaultMarkerAnnotationAccess;
import org.eclipse.ui.texteditor.ITextEditorActionDefinitionIds;
import org.eclipse.ui.texteditor.IUpdate;
import org.eclipse.ui.texteditor.MarkerAnnotationPreferences;
import org.eclipse.ui.texteditor.SourceViewerDecorationSupport;

/**
 * Text field with support for spellchecking.
 */
public class SpellcheckableMessageArea extends Composite {

    private static class TextViewerAction extends Action implements IUpdate {

        private int fOperationCode = -1;
        private ITextOperationTarget fOperationTarget;

        /**
         * Creates a new action.
         *
         * @param viewer the viewer
         * @param operationCode the opcode
         */
        public TextViewerAction(ITextViewer viewer, int operationCode) {
            fOperationCode = operationCode;
            fOperationTarget = viewer.getTextOperationTarget();
            update();
        }

        /**
         * Updates the enabled state of the action.
         * Fires a property change if the enabled state changes.
         *
         * @see Action#firePropertyChange(String, Object, Object)
         */
        public void update() {
            // XXX: workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=206111
            if (fOperationCode == ITextOperationTarget.REDO)
                return;

            boolean wasEnabled = isEnabled();
            boolean isEnabled = (fOperationTarget != null && fOperationTarget.canDoOperation(fOperationCode));
            setEnabled(isEnabled);

            if (wasEnabled != isEnabled) {
                firePropertyChange(ENABLED, wasEnabled ? Boolean.TRUE : Boolean.FALSE,
                        isEnabled ? Boolean.TRUE : Boolean.FALSE);
            }
        }

        /**
         * @see Action#run()
         */
        public void run() {
            if (fOperationCode != -1 && fOperationTarget != null) {
                fOperationTarget.doOperation(fOperationCode);
            }
        }
    }

    private final SourceViewer sourceViewer;
    private ModifyListener hardWrapModifyListener;

    /**
     * @param parent
     * @param initialText
     */
    public SpellcheckableMessageArea(Composite parent, String initialText) {
        super(parent, SWT.BORDER);
        setLayout(new FillLayout());

        AnnotationModel annotationModel = new AnnotationModel();
        sourceViewer = new SourceViewer(this, null, null, true, SWT.MULTI | SWT.V_SCROLL | SWT.WRAP);
        getTextWidget().setFont(UIUtils.getFont(UIPreferences.THEME_CommitMessageEditorFont));

        int endSpacing = 2;
        int textWidth = getCharWidth() * 70 + endSpacing;
        int textHeight = getLineHeight() * 7;
        Point size = getTextWidget().computeSize(textWidth, textHeight);
        getTextWidget().setSize(size);

        createMarginPainter();

        configureHardWrap();

        final SourceViewerDecorationSupport support = configureAnnotationPreferences();
        final IHandlerActivation handlerActivation = installQuickFixActionHandler();

        configureContextMenu();

        Document document = new Document(initialText);

        sourceViewer.configure(new TextSourceViewerConfiguration(EditorsUI.getPreferenceStore()));
        sourceViewer.setDocument(document, annotationModel);

        getTextWidget().addDisposeListener(new DisposeListener() {
            public void widgetDisposed(DisposeEvent disposeEvent) {
                support.uninstall();
                getHandlerService().deactivateHandler(handlerActivation);
            }
        });
    }

    private void configureHardWrap() {
        if (shouldHardWrap()) {
            if (hardWrapModifyListener == null) {
                final StyledText textWidget = getTextWidget();

                hardWrapModifyListener = new ModifyListener() {

                    private boolean active = true;

                    public void modifyText(ModifyEvent e) {
                        if (!active) {
                            return;
                        }
                        String lineDelimiter = textWidget.getLineDelimiter();
                        List<WrapEdit> wrapEdits = calculateWrapEdits(textWidget.getText(), 70, lineDelimiter);
                        // Prevent infinite loop because replaceTextRange causes a ModifyEvent
                        active = false;
                        for (WrapEdit wrapEdit : wrapEdits) {
                            textWidget.replaceTextRange(wrapEdit.getStart(), wrapEdit.getLength(), lineDelimiter);
                        }
                        active = true;
                    }
                };
                textWidget.addModifyListener(hardWrapModifyListener);
            }
        } else {
            if (hardWrapModifyListener != null) {
                getTextWidget().removeModifyListener(hardWrapModifyListener);
                hardWrapModifyListener = null;
            }
        }
    }

    private void configureContextMenu() {
        final TextViewerAction cutAction = new TextViewerAction(sourceViewer, ITextOperationTarget.CUT);
        cutAction.setText(UIText.SpellCheckingMessageArea_cut);
        cutAction.setActionDefinitionId(IWorkbenchCommandConstants.EDIT_CUT);

        final TextViewerAction copyAction = new TextViewerAction(sourceViewer, ITextOperationTarget.COPY);
        copyAction.setText(UIText.SpellCheckingMessageArea_copy);
        copyAction.setActionDefinitionId(IWorkbenchCommandConstants.EDIT_COPY);

        final TextViewerAction pasteAction = new TextViewerAction(sourceViewer, ITextOperationTarget.PASTE);
        pasteAction.setText(UIText.SpellCheckingMessageArea_paste);
        pasteAction.setActionDefinitionId(IWorkbenchCommandConstants.EDIT_PASTE);

        final TextViewerAction selectAllAction = new TextViewerAction(sourceViewer,
                ITextOperationTarget.SELECT_ALL);
        selectAllAction.setText(UIText.SpellCheckingMessageArea_selectAll);
        selectAllAction.setActionDefinitionId(IWorkbenchCommandConstants.EDIT_SELECT_ALL);

        MenuManager contextMenu = new MenuManager();
        contextMenu.add(cutAction);
        contextMenu.add(copyAction);
        contextMenu.add(pasteAction);
        contextMenu.add(selectAllAction);
        contextMenu.add(new Separator());

        final SubMenuManager quickFixMenu = new SubMenuManager(contextMenu);
        quickFixMenu.setVisible(true);
        quickFixMenu.addMenuListener(new IMenuListener() {
            public void menuAboutToShow(IMenuManager manager) {
                quickFixMenu.removeAll();
                addProposals(quickFixMenu);
            }
        });
        StyledText textWidget = getTextWidget();
        getTextWidget().setMenu(contextMenu.createContextMenu(textWidget));

        getTextWidget().addFocusListener(new FocusListener() {

            private IHandlerActivation cutHandlerActivation;
            private IHandlerActivation copyHandlerActivation;
            private IHandlerActivation pasteHandlerActivation;
            private IHandlerActivation selectAllHandlerActivation;

            public void focusGained(FocusEvent e) {
                cutAction.update();
                copyAction.update();
                IHandlerService service = (IHandlerService) PlatformUI.getWorkbench()
                        .getService(IHandlerService.class);
                this.cutHandlerActivation = service.activateHandler(IWorkbenchCommandConstants.EDIT_CUT,
                        new ActionHandler(cutAction), new ActiveShellExpression(getParent().getShell()));
                this.copyHandlerActivation = service.activateHandler(IWorkbenchCommandConstants.EDIT_COPY,
                        new ActionHandler(copyAction), new ActiveShellExpression(getParent().getShell()));
                this.pasteHandlerActivation = service.activateHandler(IWorkbenchCommandConstants.EDIT_PASTE,
                        new ActionHandler(pasteAction), new ActiveShellExpression(getParent().getShell()));
                this.selectAllHandlerActivation = service.activateHandler(
                        IWorkbenchCommandConstants.EDIT_SELECT_ALL, new ActionHandler(selectAllAction),
                        new ActiveShellExpression(getParent().getShell()));
            }

            /* (non-Javadoc)
             * @see org.eclipse.swt.events.FocusAdapter#focusLost(org.eclipse.swt.events.FocusEvent)
             */
            public void focusLost(FocusEvent e) {
                IHandlerService service = (IHandlerService) PlatformUI.getWorkbench()
                        .getService(IHandlerService.class);

                if (cutHandlerActivation != null) {
                    service.deactivateHandler(cutHandlerActivation);
                }

                if (copyHandlerActivation != null) {
                    service.deactivateHandler(copyHandlerActivation);
                }

                if (pasteHandlerActivation != null) {
                    service.deactivateHandler(pasteHandlerActivation);
                }

                if (selectAllHandlerActivation != null) {
                    service.deactivateHandler(selectAllHandlerActivation);
                }
            }

        });

        sourceViewer.addSelectionChangedListener(new ISelectionChangedListener() {

            public void selectionChanged(SelectionChangedEvent event) {
                cutAction.update();
                copyAction.update();
            }

        });
    }

    private void addProposals(final SubMenuManager quickFixMenu) {
        IAnnotationModel sourceModel = sourceViewer.getAnnotationModel();
        Iterator annotationIterator = sourceModel.getAnnotationIterator();
        while (annotationIterator.hasNext()) {
            Annotation annotation = (Annotation) annotationIterator.next();
            boolean isDeleted = annotation.isMarkedDeleted();
            boolean isIncluded = includes(sourceModel.getPosition(annotation), getTextWidget().getCaretOffset());
            boolean isFixable = sourceViewer.getQuickAssistAssistant().canFix(annotation);
            if (!isDeleted && isIncluded && isFixable) {
                IQuickAssistProcessor processor = sourceViewer.getQuickAssistAssistant().getQuickAssistProcessor();
                IQuickAssistInvocationContext context = sourceViewer.getQuickAssistInvocationContext();
                ICompletionProposal[] proposals = processor.computeQuickAssistProposals(context);

                for (ICompletionProposal proposal : proposals)
                    quickFixMenu.add(createQuickFixAction(proposal));
            }
        }
    }

    private boolean includes(Position position, int caretOffset) {
        return position.includes(caretOffset) || (position.offset + position.length) == caretOffset;
    }

    private IAction createQuickFixAction(final ICompletionProposal proposal) {
        return new Action(proposal.getDisplayString()) {

            public void run() {
                proposal.apply(sourceViewer.getDocument());
            }

            public ImageDescriptor getImageDescriptor() {
                Image image = proposal.getImage();
                if (image != null) {
                    return ImageDescriptor.createFromImage(image);
                }
                return null;
            }
        };
    }

    private IHandlerService getHandlerService() {
        final IHandlerService handlerService = (IHandlerService) PlatformUI.getWorkbench()
                .getService(IHandlerService.class);
        return handlerService;
    }

    private SourceViewerDecorationSupport configureAnnotationPreferences() {
        ISharedTextColors textColors = EditorsUI.getSharedTextColors();
        IAnnotationAccess annotationAccess = new DefaultMarkerAnnotationAccess();
        final SourceViewerDecorationSupport support = new SourceViewerDecorationSupport(sourceViewer, null,
                annotationAccess, textColors);

        List annotationPreferences = new MarkerAnnotationPreferences().getAnnotationPreferences();
        Iterator e = annotationPreferences.iterator();
        while (e.hasNext())
            support.setAnnotationPreference((AnnotationPreference) e.next());

        support.install(EditorsUI.getPreferenceStore());
        return support;
    }

    private void createMarginPainter() {
        MarginPainter marginPainter = new MarginPainter(sourceViewer);
        marginPainter.setMarginRulerColumn(70);
        marginPainter.setMarginRulerColor(Display.getDefault().getSystemColor(SWT.COLOR_GRAY));
        sourceViewer.addPainter(marginPainter);
    }

    private int getCharWidth() {
        GC gc = new GC(getTextWidget());
        int charWidth = gc.getFontMetrics().getAverageCharWidth();
        gc.dispose();
        return charWidth;
    }

    private int getLineHeight() {
        return getTextWidget().getLineHeight();
    }

    /**
     * @return if the commit message should be hard-wrapped (preference)
     */
    private static boolean shouldHardWrap() {
        return Activator.getDefault().getPreferenceStore()
                .getBoolean(UIPreferences.COMMIT_DIALOG_HARD_WRAP_MESSAGE);
    }

    /**
     * @return widget
     */
    public StyledText getTextWidget() {
        return sourceViewer.getTextWidget();
    }

    private IHandlerActivation installQuickFixActionHandler() {
        IHandlerService handlerService = getHandlerService();
        ActionHandler handler = createQuickFixActionHandler(sourceViewer);
        ActiveShellExpression expression = new ActiveShellExpression(sourceViewer.getTextWidget().getShell());
        return handlerService.activateHandler(ITextEditorActionDefinitionIds.QUICK_ASSIST, handler, expression);
    }

    private ActionHandler createQuickFixActionHandler(final ITextOperationTarget textOperationTarget) {
        Action quickFixAction = new Action() {
            /*
             * (non-Javadoc)
             *
             * @see org.eclipse.jface.action.Action#run()
             */
            public void run() {
                textOperationTarget.doOperation(ISourceViewer.QUICK_ASSIST);
            }
        };
        quickFixAction.setActionDefinitionId(ITextEditorActionDefinitionIds.QUICK_ASSIST);
        return new ActionHandler(quickFixAction);
    }

    /**
     * Return the commit message, converting platform-specific line endings.
     *
     * @return commit message
     */
    public String getCommitMessage() {
        String text = getText();
        text = text.replaceAll(getTextWidget().getLineDelimiter(), "\n"); //$NON-NLS-1$
        return text;
    }

    /**
     * Reconfigure this widget if a preference has changed.
     */
    public void reconfigure() {
        configureHardWrap();
    }

    /**
     * @return text
     */
    public String getText() {
        return getTextWidget().getText();
    }

    /**
     * @param text
     */
    public void setText(String text) {
        getTextWidget().setText(text);
    }

    /**
     *
     */
    public boolean setFocus() {
        return getTextWidget().setFocus();
    }

    /**
     * Calculate a list of {@link WrapEdit} which can be applied to the text to
     * get a new text that is wrapped at word boundaries. Existing line breaks
     * are left alone (text is not reflowed).
     *
     * @param text
     *            the text to calculate the wrap edits for
     * @param maxLineLength
     *            the maximum line length
     * @param lineDelimiter
     *            line delimiter used in text and for wrapping
     * @return a list of {@link WrapEdit} objects which specify how the text
     *         should be edited to obtain the wrapped text. Offsets of later
     *         edits are already adjusted for the fact that wrapping a line may
     *         shift the text backwards. So the list can just be iterated and
     *         each edit applied in order.
     */
    public static List<WrapEdit> calculateWrapEdits(final String text, final int maxLineLength,
            final String lineDelimiter) {
        List<WrapEdit> wrapEdits = new LinkedList<WrapEdit>();

        int offset = 0;
        int lineDelimiterLength = lineDelimiter.length();

        String[] chunks = text.split(lineDelimiter, -1);
        for (int chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
            String chunk = chunks[chunkIndex];

            String[] words = chunk.split(" ", -1); //$NON-NLS-1$
            int lineLength = 0;

            for (int wordIndex = 0; wordIndex < words.length; wordIndex++) {
                String word = words[wordIndex];

                int wordLength = word.length();
                int newLineLength = lineLength + wordLength + 1 /* the space */;
                if (newLineLength > maxLineLength) {
                    /* don't break before a single long word */
                    if (lineLength != 0) {
                        wrapEdits.add(new WrapEdit(offset, 1));
                        /* adjust for the shifting of text after the edit is applied */
                        offset += lineDelimiterLength;
                    }
                    lineLength = 0;
                } else if (wordIndex != 0) {
                    lineLength += 1;
                    offset += 1;
                }
                offset += wordLength;
                lineLength += wordLength;
            }

            if (chunkIndex != chunks.length - 1) {
                offset += lineDelimiterLength;
            }
        }

        return wrapEdits;
    }

    /**
     * Edit for replacing a space with a line delimiter to wrap a long line.
     */
    public static class WrapEdit {
        private int start;
        private int length;

        /**
         * @param start see {@link #getStart()}
         * @param length see {@link #getLength()}
         */
        public WrapEdit(int start, int length) {
            this.start = start;
            this.length = length;
        }

        /**
         * @return character offset of where the edit should be applied on the
         *         text
         */
        public int getStart() {
            return start;
        }

        /**
         * @return number of characters which should be replaced by the line
         *         delimiter
         */
        public int getLength() {
            return length;
        }
    }
}