/*******************************************************************************
* Copyright (c) 2009 Andrey Loskutov.
* 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
* Contributor: Andrey Loskutov - initial API and implementation
*******************************************************************************/
package de.loskutov.anyedit.actions;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.dialogs.IPageChangedListener;
import org.eclipse.jface.dialogs.PageChangedEvent;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.AnnotationModel;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.text.source.IAnnotationModelExtension;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.IPostSelectionProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.ITreeSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IPartListener;
import org.eclipse.ui.IPropertyListener;
import org.eclipse.ui.ISaveablePart;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.forms.editor.FormEditor;
import org.eclipse.ui.handlers.IHandlerService;
import org.eclipse.ui.texteditor.IDocumentProvider;
import de.loskutov.anyedit.AnyEditToolsPlugin;
import de.loskutov.anyedit.IAnyEditConstants;
import de.loskutov.anyedit.ui.editor.AbstractEditor;
import de.loskutov.anyedit.util.EclipseUtils;
public class ToggleWhitespace extends AbstractAction {
private static final String SHOW_WS_COMMAND = "AnyEdit.ShowWhiteSpace.command";
private static final String SPACES = "AnyEditTools.spaces";
private static final String TABS = "AnyEditTools.tabs";
private static final String TRAILING = "AnyEditTools.trailingws";
private IAction proxyAction;
private SuperListener mainListener;
public ToggleWhitespace() {
super();
}
@Override
public void init(IWorkbenchWindow window1) {
super.init(window1);
mainListener = installGlobalListener(window1);
// TODO if we are activated on startup, should get the action from toolbar and set the state
}
@Override
public void run(IAction action) {
super.run(action);
this.proxyAction = action;
boolean newValue = !isChecked();
setChecked(newValue);
applyEditorAnnotations(newValue);
// XXX update the "toggled" state based on the current editor
mainListener.ws.updateCheckState();
}
SuperListener installGlobalListener(IWorkbenchWindow window1){
IHandlerService hs = (IHandlerService) PlatformUI.getWorkbench().getService(IHandlerService.class);
Object variable = hs.getCurrentState().getRoot().getVariable(SHOW_WS_COMMAND);
if(variable instanceof SuperListener) {
return (SuperListener) variable;
}
SuperListener superListener = new SuperListener(this);
hs.getCurrentState().getRoot().addVariable(SHOW_WS_COMMAND, superListener);
IWorkbenchPage activePage = window1.getActivePage();
activePage.addPartListener(superListener);
IWorkbenchPart activePart = activePage.getActivePart();
if (activePart != null) {
superListener.partActivated(activePart);
}
AnyEditToolsPlugin.getDefault().getPreferenceStore().addPropertyChangeListener(superListener);
return superListener;
}
void uninstallGlobalListener(){
AnyEditToolsPlugin.getDefault().getPreferenceStore().removePropertyChangeListener(mainListener);
IHandlerService hs = (IHandlerService) PlatformUI.getWorkbench().getService(IHandlerService.class);
Object variable = hs.getCurrentState().getRoot().getVariable(SHOW_WS_COMMAND);
if(variable == null || variable.equals(Boolean.FALSE)) {
return;
}
hs.getCurrentState().getRoot().addVariable(SHOW_WS_COMMAND, Boolean.FALSE);
IWorkbenchPage[] pages = getWindow().getPages();
for (int i = 0; i < pages.length; i++) {
pages[i].removePartListener(mainListener);
}
}
@Override
public void dispose() {
// causes NPE if no active page is there but dispose is called
// window.getActivePage().removePartListener(this);
if(getWindow() == null){
// strange init issue, just return
return;
}
proxyAction = null;
uninstallGlobalListener();
super.dispose();
}
private void applyEditorAnnotations(boolean on) {
AbstractEditor aEditor = getEditor();
if (aEditor == null) {
// XXX ?
// disableButton();
return;
}
IDocumentProvider documentProvider = aEditor.getDocumentProvider();
if (documentProvider == null) {
// XXX ?
// disableButton();
return;
}
IEditorInput input = aEditor.getInput();
IAnnotationModel annotationModel = documentProvider.getAnnotationModel(input);
if (!(annotationModel instanceof IAnnotationModelExtension)) {
return;
}
final IAnnotationModelExtension extension = (IAnnotationModelExtension) annotationModel;
if (!on) {
removeAnnotations(extension);
return;
}
addAnnotations(aEditor, extension);
}
private static void addAnnotations(final AbstractEditor aEditor,
final IAnnotationModelExtension extension) {
final AnnotationModel annotationModel = new AnnotationModel();
final IDocument doc = aEditor.getDocument();
final int lines = doc.getNumberOfLines();
final Job job = new Job("Toggle whitespace") {
@Override
public IStatus run(IProgressMonitor monitor) {
if(aEditor.isDisposed() || aEditor.getPart() == null){
return Status.CANCEL_STATUS;
}
monitor.beginTask("Whitespace annotation ...", lines);
extension.removeAnnotationModel(ToggleWhitespace.class);
final Annotation sAnnotation = new Annotation(SPACES, false, "Spaces");
final Annotation tAnnotation = new Annotation(TABS, false, "Tabs");
boolean showTrailingDifferently = isTrailingShownDifferently();
for (int i = 0; i < lines && !monitor.isCanceled(); i++) {
monitor.internalWorked(1);
try {
IRegion region = doc.getLineInformation(i);
String line = doc.get(region.getOffset(), region.getLength());
if(showTrailingDifferently && line.length() > 0 && line.trim().length() == 0){
addTrailingAnnotation(annotationModel, line, region);
} else {
addAnnotations(annotationModel, line, region, ' ', sAnnotation, monitor);
addAnnotations(annotationModel, line, region, '\t', tAnnotation, monitor);
}
} catch (BadLocationException e) {
AnyEditToolsPlugin.logError(
"Problem during annotation of whitespace", e);
}
}
if (!monitor.isCanceled()) {
extension.addAnnotationModel(ToggleWhitespace.class, annotationModel);
}
monitor.done();
return monitor.isCanceled() ? Status.CANCEL_STATUS : Status.OK_STATUS;
}
};
job.setUser(true);
job.setPriority(Job.INTERACTIVE);
job.schedule();
}
private static void addAnnotations(IAnnotationModel annotationModel, String line,
IRegion region, char c, final Annotation annotation, IProgressMonitor monitor) {
int startIdx = line.indexOf(c, 0);
int stopIdx = startIdx;
boolean showTrailingDifferently = isTrailingShownDifferently();
boolean showTrailingOnly = isTrailingOnly();
while (stopIdx >= 0 && !monitor.isCanceled()) {
int oldStopIdx = stopIdx;
stopIdx = line.indexOf(c, stopIdx + 1);
if (stopIdx == oldStopIdx + 1) {
continue;
}
if ((stopIdx > oldStopIdx + 1 || stopIdx < 0) && oldStopIdx - startIdx >= 0) {
int offset = region.getOffset() + startIdx;
int length = oldStopIdx - startIdx + 1;
if (true /*length > 1 || c == '\t'*/) {
Annotation lineAnnotation = null;
Position position = new Position(offset, length);
if(oldStopIdx == line.length() - 1) {
if (showTrailingDifferently) {
// on the same line editor must have different annotation object
lineAnnotation = new Annotation(TRAILING, false, "Trailing whitespace");
} else {
// on the same line editor must have different annotation object
lineAnnotation = new Annotation(annotation.getType(), false,
annotation.getText());
}
} else if(!showTrailingOnly){
// on the same line editor must have different annotation object
lineAnnotation = new Annotation(annotation.getType(), false,
annotation.getText());
}
if(lineAnnotation != null) {
annotationModel.addAnnotation(lineAnnotation, position);
}
}
startIdx = stopIdx;
}
}
}
private static void addTrailingAnnotation(IAnnotationModel annotationModel, String line,
IRegion region) {
int offset = region.getOffset();
int length = line.length();
Position position = new Position(offset, length);
Annotation lineAnnotation = new Annotation(TRAILING, false, "Trailing whitespace");
annotationModel.addAnnotation(lineAnnotation, position);
}
private static void removeAnnotations(final IAnnotationModelExtension extension) {
final Job job = new Job("Toggle whitespace") {
@Override
public IStatus run(IProgressMonitor monitor) {
monitor.beginTask("Removing whitespace annotations",
IProgressMonitor.UNKNOWN);
extension.removeAnnotationModel(ToggleWhitespace.class);
extension.addAnnotationModel(ToggleWhitespace.class,
new AnnotationModel());
monitor.done();
return monitor.isCanceled() ? Status.CANCEL_STATUS : Status.OK_STATUS;
}
};
job.setUser(true);
job.setPriority(Job.INTERACTIVE);
job.schedule();
}
static class SuperListener implements IPartListener, ISelectionChangedListener,
IPageChangedListener, IPropertyListener, IPropertyChangeListener {
private final ToggleWhitespace ws;
public SuperListener(ToggleWhitespace ws) {
this.ws = ws;
}
@Override
public void partActivated(IWorkbenchPart part) {
if (!(part instanceof IEditorPart)) {
ws.disableButton();
return;
}
IEditorPart activeEditor = EclipseUtils.getActiveEditor();
if(activeEditor != part){
ws.disableButton();
return;
}
AbstractEditor abstractEditor = ws.createActiveEditorDelegate();
ws.setEditor(abstractEditor);
if (abstractEditor.isMultiPage()) {
addPageListener(part);
}
IDocumentProvider documentProvider = abstractEditor.getDocumentProvider();
if (documentProvider == null) {
ws.disableButton();
return;
}
ws.enableButton();
part.addPropertyListener(this);
ws.applyEditorAnnotations(ToggleWhitespace.isChecked());
}
@Override
public void partDeactivated(IWorkbenchPart part) {
if (!(part instanceof IEditorPart)) {
return;
}
removePageListener(part);
part.removePropertyListener(this);
}
@Override
public void partBroughtToTop(IWorkbenchPart part) {
// ignore
}
@Override
public void partOpened(IWorkbenchPart part) {
// not used
}
@Override
public void partClosed(IWorkbenchPart part) {
if (!(part instanceof IEditorPart)) {
return;
}
if (ws.proxyAction != null && ws.proxyAction.isEnabled()) {
boolean hasEditors = part.getSite().getPage().getEditorReferences().length > 0;
if (!hasEditors) {
ws.disableButton();
}
}
}
/**
* @param part expected to be a multi page part, never null
*/
private void addPageListener(IWorkbenchPart part) {
if (part instanceof FormEditor) {
FormEditor formEditor = (FormEditor) part;
formEditor.addPageChangedListener(this);
} else {
ISelectionProvider selectionProvider = part.getSite().getSelectionProvider();
if (selectionProvider instanceof IPostSelectionProvider) {
((IPostSelectionProvider) selectionProvider)
.addSelectionChangedListener(this);
}
}
}
/**
* @param part must be non null
*/
private void removePageListener(IWorkbenchPart part) {
if (part instanceof FormEditor) {
FormEditor formEditor = (FormEditor) part;
formEditor.removePageChangedListener(this);
} else {
ISelectionProvider selectionProvider = part.getSite().getSelectionProvider();
if (selectionProvider instanceof IPostSelectionProvider) {
((IPostSelectionProvider) selectionProvider)
.removeSelectionChangedListener(this);
}
}
}
/**
* to catch the page selection in multi page editors which do not extend FormEditor
*/
@Override
public void selectionChanged(SelectionChangedEvent event) {
ISelection selection = event.getSelection();
if (!(selection instanceof ITextSelection)) {
// TODO this would prevent disabling of the button on web tools xml editor
// but it does not have sense at all because they do not support highlighting,
// only underligning
// see https://bugs.eclipse.org/bugs/show_bug.cgi?id=156086
if (!(selection instanceof ITreeSelection)) {
ws.disableButton();
}
return;
}
if(ws.getWindow() == null){
// strange init issue, just return
return;
}
AbstractEditor abstractEditor = ws.getEditor();
IEditorPart editorPart = ws.getWindow().getActivePage().getActiveEditor();
if (!new AbstractEditor(editorPart).equals(abstractEditor)) {
partActivated(editorPart);
}
}
/**
* to catch the page selection in multi page editors
*/
@Override
public void pageChanged(PageChangedEvent event) {
if(ws.getWindow() == null){
// strange init issue, just return
return;
}
partActivated(ws.getWindow().getActivePage().getActiveEditor());
}
/**
* to catch the dirty state
*/
@Override
public void propertyChanged(Object source, int propId) {
if (propId != ISaveablePart.PROP_DIRTY) {
return;
}
if (ws.proxyAction != null && ws.proxyAction.isChecked()) {
AbstractEditor abstractEditor = ws.getEditor();
if (abstractEditor != null && !abstractEditor.isDirty()) {
ws.applyEditorAnnotations(ToggleWhitespace.isChecked());
}
}
}
@Override
public void propertyChange(PropertyChangeEvent event) {
String key = event.getProperty();
if (!IAnyEditConstants.SHOW_TRAILING_ONLY.equals(key)
&& !IAnyEditConstants.SHOW_TRAILING_DIFFERENTLY.equals(key)) {
return;
}
if(ToggleWhitespace.isChecked()){
ws.applyEditorAnnotations(true);
}
}
}
private void enableButton() {
if (proxyAction != null) {
proxyAction.setEnabled(true);
}
}
private void disableButton() {
if (proxyAction != null) {
proxyAction.setEnabled(false);
}
setEditor(null);
}
@Override
public void selectionChanged(IAction action, ISelection selection) {
this.proxyAction = action;
updateCheckState();
}
private void updateCheckState() {
if(proxyAction == null) {
return;
}
boolean checked = isChecked();
if(checked != proxyAction.isChecked()) {
// XXX does not work with commands
proxyAction.setChecked(checked);
}
IEditorPart activeEditor = EclipseUtils.getActiveEditor();
if(activeEditor == null){
disableButton();
return;
}
}
protected static boolean isChecked() {
return getPrefs().getBoolean(IAnyEditConstants.SHOW_WHITESPACE);
}
private static IPreferenceStore getPrefs() {
return AnyEditToolsPlugin.getDefault().getPreferenceStore();
}
protected static void setChecked(boolean checked) {
getPrefs().setValue(IAnyEditConstants.SHOW_WHITESPACE, checked);
}
protected static boolean isTrailingOnly() {
return getPrefs().getBoolean(IAnyEditConstants.SHOW_TRAILING_ONLY);
}
protected static boolean isTrailingShownDifferently() {
return getPrefs().getBoolean(IAnyEditConstants.SHOW_TRAILING_DIFFERENTLY);
}
}