| /*=============================================================================# |
| # Copyright (c) 2008, 2021 Stephan Wahlbrink and others. |
| # |
| # This program and the accompanying materials are made available under the |
| # terms of the Eclipse Public License 2.0 which is available at |
| # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 |
| # which is available at https://www.apache.org/licenses/LICENSE-2.0. |
| # |
| # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 |
| # |
| # Contributors: |
| # Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation |
| #=============================================================================*/ |
| |
| package org.eclipse.statet.ltk.ui; |
| |
| import static org.eclipse.statet.ltk.ui.LtkUI.BUNDLE_ID; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| 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.viewers.IPostSelectionProvider; |
| import org.eclipse.jface.viewers.ISelectionChangedListener; |
| import org.eclipse.jface.viewers.SelectionChangedEvent; |
| import org.eclipse.ui.statushandlers.StatusManager; |
| |
| import org.eclipse.statet.jcommons.collections.CopyOnWriteIdentityListSet; |
| import org.eclipse.statet.jcommons.collections.ImIdentityList; |
| |
| import org.eclipse.statet.ltk.model.core.element.LtkModelElement; |
| import org.eclipse.statet.ltk.model.core.element.LtkModelElementDelta; |
| import org.eclipse.statet.ltk.model.core.element.SourceUnit; |
| |
| |
| /** |
| * Controller implementation combining {@link IPostSelectionProvider} and |
| * {@link ModelElementInputProvider} to provide support for |
| * {@link SelectionWithElementInfoListener}. |
| */ |
| public class PostSelectionWithElementInfoController { |
| |
| |
| private class SelectionTask extends Job { |
| |
| |
| private final class Data extends LTKInputData { |
| |
| int stateNr; |
| |
| Data(final SourceUnit input, final SelectionChangedEvent currentSelection, final int runNr) { |
| super(input, (currentSelection != null) ? currentSelection.getSelection() : null); |
| this.stateNr= runNr; |
| } |
| |
| @Override |
| public boolean isStillValid() { |
| return (PostSelectionWithElementInfoController.this.currentNr == this.stateNr); |
| } |
| |
| } |
| |
| private int lastNr; |
| |
| |
| public SelectionTask() { |
| super("PostSelection with Model Updater"); // //$NON-NLS-1$ |
| setPriority(Job.SHORT); |
| setSystem(true); |
| setUser(false); |
| |
| this.lastNr= PostSelectionWithElementInfoController.this.currentNr= Integer.MIN_VALUE; |
| } |
| |
| |
| @Override |
| protected synchronized IStatus run(final IProgressMonitor monitor) { |
| SourceUnit input= null; |
| try { |
| checkNewInput(); |
| |
| final Data run; |
| IgnoreActivation[] ignore= null; |
| synchronized (PostSelectionWithElementInfoController.this.inputLock) { |
| run= new Data(PostSelectionWithElementInfoController.this.input, PostSelectionWithElementInfoController.this.currentSelection, PostSelectionWithElementInfoController.this.currentNr); |
| if (run.inputElement == null || run.selection == null |
| || (this.lastNr == run.stateNr && PostSelectionWithElementInfoController.this.newListeners.isEmpty())) { |
| return Status.OK_STATUS; |
| } |
| |
| if (!PostSelectionWithElementInfoController.this.ignoreList.isEmpty()) { |
| int num= PostSelectionWithElementInfoController.this.ignoreList.size(); |
| ignore= PostSelectionWithElementInfoController.this.ignoreList.toArray(new IgnoreActivation[num]); |
| for (int i= num-1; i >= 0; i--) { |
| if (ignore[i].marked && ignore[i].nr != run.stateNr) { |
| PostSelectionWithElementInfoController.this.ignoreList.remove(i); |
| ignore[i]= null; |
| num--; |
| } |
| } |
| if (num == 0) { |
| ignore= null; |
| } |
| } |
| |
| input= run.inputElement; |
| input.connect(monitor); |
| } |
| if (run.getInputInfo() == null |
| || run.getInputInfo().getStamp().getContentStamp() != input.getDocument(null).getModificationStamp()) { |
| return Status.OK_STATUS; |
| } |
| |
| ImIdentityList<SelectionWithElementInfoListener> listeners= PostSelectionWithElementInfoController.this.newListeners.clearToList(); |
| if (run.stateNr != this.lastNr) { |
| listeners= PostSelectionWithElementInfoController.this.listeners.toList(); |
| this.lastNr= run.stateNr; |
| } |
| ITER_LISTENER: for (final SelectionWithElementInfoListener listener : listeners) { |
| if (ignore != null) { |
| for (int j= 0; j < ignore.length; j++) { |
| if (ignore[j] != null && ignore[j].listener == listener) { |
| continue ITER_LISTENER; |
| } |
| } |
| } |
| if (!run.isStillValid()) { |
| return Status.CANCEL_STATUS; |
| } |
| try { |
| listener.stateChanged(run); |
| } |
| catch (final Exception e) { |
| logListenerError(e); |
| } |
| } |
| } |
| finally { |
| if (input != null) { |
| input.disconnect(monitor); |
| } |
| } |
| |
| return Status.OK_STATUS; |
| } |
| |
| private void checkNewInput() { |
| if (PostSelectionWithElementInfoController.this.inputChanged) { |
| synchronized (PostSelectionWithElementInfoController.this.inputLock) { |
| PostSelectionWithElementInfoController.this.inputChanged= false; |
| } |
| final ImIdentityList<SelectionWithElementInfoListener> listeners= PostSelectionWithElementInfoController.this.listeners.toList(); |
| for (final SelectionWithElementInfoListener listener : listeners) { |
| try { |
| listener.inputChanged(); |
| } |
| catch (final Exception e) { |
| logListenerError(e); |
| } |
| } |
| } |
| } |
| } |
| |
| private class SelectionListener implements ISelectionChangedListener { |
| |
| private boolean active; |
| |
| @Override |
| public void selectionChanged(final SelectionChangedEvent event) { |
| if (!this.active) { |
| return; |
| } |
| synchronized (PostSelectionWithElementInfoController.this) { |
| if (PostSelectionWithElementInfoController.this.currentSelection != null && PostSelectionWithElementInfoController.this.currentSelection.getSelection().equals(event.getSelection())) { |
| return; |
| } |
| PostSelectionWithElementInfoController.this.currentNr++; |
| PostSelectionWithElementInfoController.this.currentSelection= event; |
| PostSelectionWithElementInfoController.this.updateJob.schedule(); |
| } |
| } |
| } |
| |
| public class IgnoreActivation { |
| |
| private final SelectionWithElementInfoListener listener; |
| private boolean marked; |
| private int nr; |
| |
| private IgnoreActivation(final SelectionWithElementInfoListener listener) { |
| this.listener= listener; |
| } |
| |
| public void deleteNext() { |
| this.nr= PostSelectionWithElementInfoController.this.currentNr; |
| this.marked= true; |
| } |
| |
| public void delete() { |
| this.nr= PostSelectionWithElementInfoController.this.currentNr-1; |
| this.marked= true; |
| synchronized (PostSelectionWithElementInfoController.this.inputLock) { |
| PostSelectionWithElementInfoController.this.ignoreList.remove(this); |
| } |
| } |
| |
| } |
| |
| private final IPostSelectionProvider selectionProvider; |
| private final ModelElementInputProvider modelProvider; |
| private final CopyOnWriteIdentityListSet<SelectionWithElementInfoListener> listeners= new CopyOnWriteIdentityListSet<>(); |
| private final CopyOnWriteIdentityListSet<SelectionWithElementInfoListener> newListeners= new CopyOnWriteIdentityListSet<>(); |
| private final Object inputLock= new Object(); |
| |
| private final ModelElementInputListener elementChangeListener; |
| private final SelectionListener selectionListener; |
| private final SelectionListener postSelectionListener; |
| private PostSelectionCancelExtension cancelExtension; |
| |
| private final List<IgnoreActivation> ignoreList= new ArrayList<>(); |
| |
| private SourceUnit input; // current input |
| private boolean inputChanged; |
| private SelectionChangedEvent currentSelection; // current selection |
| private volatile int currentNr; // stamp to check, if information still up-to-date |
| |
| private final SelectionTask updateJob= new SelectionTask(); |
| |
| |
| public PostSelectionWithElementInfoController(final ModelElementInputProvider modelProvider, |
| final IPostSelectionProvider selectionProvider, final PostSelectionCancelExtension cancelExt) { |
| this.selectionProvider= selectionProvider; |
| |
| this.modelProvider= modelProvider; |
| |
| this.elementChangeListener= new ModelElementInputListener() { |
| @Override |
| public void elementChanged(final LtkModelElement element) { |
| synchronized (PostSelectionWithElementInfoController.this.inputLock) { |
| if (PostSelectionWithElementInfoController.this.updateJob.getState() == Job.WAITING) { |
| PostSelectionWithElementInfoController.this.updateJob.cancel(); |
| } |
| PostSelectionWithElementInfoController.this.input= (SourceUnit) element; |
| PostSelectionWithElementInfoController.this.inputChanged= true; |
| PostSelectionWithElementInfoController.this.currentNr++; |
| PostSelectionWithElementInfoController.this.currentSelection= null; |
| PostSelectionWithElementInfoController.this.updateJob.schedule(); |
| } |
| } |
| @Override |
| public void elementInitialInfo(final LtkModelElement element) { |
| checkUpdate(element); |
| } |
| @Override |
| public void elementUpdatedInfo(final LtkModelElement element, final LtkModelElementDelta delta) { |
| checkUpdate(element); |
| } |
| private void checkUpdate(final LtkModelElement element) { |
| synchronized (PostSelectionWithElementInfoController.this.inputLock) { |
| PostSelectionWithElementInfoController.this.currentNr++; |
| if (PostSelectionWithElementInfoController.this.currentSelection == null) { |
| return; |
| } |
| } |
| PostSelectionWithElementInfoController.this.updateJob.run(null); |
| } |
| }; |
| this.selectionListener= new SelectionListener(); |
| this.selectionListener.active= false; |
| this.selectionProvider.addSelectionChangedListener(this.selectionListener); |
| |
| this.postSelectionListener= new SelectionListener(); |
| this.postSelectionListener.active= true; |
| this.selectionProvider.addPostSelectionChangedListener(this.postSelectionListener); |
| |
| this.modelProvider.addListener(this.elementChangeListener); |
| if (cancelExt != null) { |
| this.cancelExtension= cancelExt; |
| this.cancelExtension.controller= this; |
| this.cancelExtension.init(); |
| } |
| } |
| |
| |
| public void setUpdateOnSelection(final boolean active) { |
| this.selectionListener.active= active; |
| } |
| |
| public void setUpdateOnPostSelection(final boolean active) { |
| this.postSelectionListener.active= active; |
| } |
| |
| public void cancel() { |
| synchronized (this.inputLock) { |
| this.currentNr++; |
| this.currentSelection= null; |
| } |
| } |
| |
| public void dispose() { |
| cancel(); |
| this.modelProvider.removeListener(this.elementChangeListener); |
| this.selectionProvider.removeSelectionChangedListener(this.selectionListener); |
| this.selectionProvider.removePostSelectionChangedListener(this.postSelectionListener); |
| if (this.cancelExtension != null) { |
| this.cancelExtension.dispose(); |
| } |
| this.newListeners.clear(); |
| this.listeners.clear(); |
| } |
| |
| |
| public void addListener(final SelectionWithElementInfoListener listener) { |
| this.listeners.add(listener); |
| this.newListeners.add(listener); |
| this.updateJob.schedule(); |
| } |
| |
| public void removeListener(final SelectionWithElementInfoListener listener) { |
| this.newListeners.remove(listener); |
| this.listeners.remove(listener); |
| } |
| |
| public IgnoreActivation ignoreNext(final SelectionWithElementInfoListener listener) { |
| final IgnoreActivation control= new IgnoreActivation(listener); |
| this.ignoreList.add(control); |
| return control; |
| } |
| |
| |
| private void logListenerError(final Throwable e) { |
| StatusManager.getManager().handle(new Status(IStatus.ERROR, BUNDLE_ID, 0, |
| "An error occurred when calling a registered listener.", e )); //$NON-NLS-1$ |
| } |
| |
| } |