Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: deda8adbe61a647255fda45d582f2da835dbca34 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
/*******************************************************************************
 * Copyright (c) 2005, 2016 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.ui.internal.texteditor;


import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.custom.VerifyKeyListener;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.VerifyEvent;

import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.IExecutionListener;
import org.eclipse.core.commands.NotHandledException;

import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.Status;

import org.eclipse.jface.text.ITextViewer;

import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.commands.ICommandService;

/**
 * Exit strategy for commands that want to fold repeated execution into one compound edit. See
 * {@link org.eclipse.jface.text.IRewriteTarget#endCompoundChange() IRewriteTarget.endCompoundChange}.
 * As long as a strategy is installed on an {@link ITextViewer}, it will detect the end of a
 * compound operation when any of the following conditions becomes true:
 * <ul>
 * <li>the viewer's text widget loses the keyboard focus</li>
 * <li>the mouse is clicked or double clicked inside the viewer's widget</li>
 * <li>a command other than the ones specified is executed</li>
 * <li>the viewer receives any key events that are not modifier combinations</li>
 * </ul>
 * <p>
 * If the end of a compound edit is detected, any registered {@link ICompoundEditListener}s are
 * notified and the strategy is disarmed (spring-loaded).
 * </p>
 *
 * @since 3.1
 */
public final class CompoundEditExitStrategy {
	/**
	 * Listens for events that may trigger the end of a compound edit.
	 */
	private final class EventListener implements MouseListener, FocusListener, VerifyKeyListener, IExecutionListener {

		@Override
		public void mouseDoubleClick(MouseEvent e) {
			// mouse actions end the compound change
			fireEndCompoundEdit();
		}

		@Override
		public void mouseDown(MouseEvent e) {
			// mouse actions end the compound change
			fireEndCompoundEdit();
		}

		@Override
		public void mouseUp(MouseEvent e) {}

		@Override
		public void focusGained(FocusEvent e) {}

		@Override
		public void focusLost(FocusEvent e) {
			// losing focus ends the change
			fireEndCompoundEdit();
		}

		@Override
		public void notHandled(String commandId, NotHandledException exception) {}

		@Override
		public void postExecuteFailure(String commandId, ExecutionException exception) {}

		@Override
		public void postExecuteSuccess(String commandId, Object returnValue) {}

		@Override
		public void preExecute(String commandId, ExecutionEvent event) {
			// any command other than the known ones end the compound change
			for (int i= 0; i < fCommandIds.length; i++) {
				if (commandId.equals(fCommandIds[i]))
					return;
			}
			fireEndCompoundEdit();
		}

		@Override
		public void verifyKey(VerifyEvent event) {
			// any key press that is not a modifier combo ends the compound change
			final int maskWithoutShift= SWT.MODIFIER_MASK & ~SWT.SHIFT;
			if ((event.keyCode & SWT.MODIFIER_MASK) == 0 && (event.stateMask & maskWithoutShift) == 0)
				fireEndCompoundEdit();
		}

	}

	private final String[] fCommandIds;
	private final EventListener fEventListener= new EventListener();

	private final ListenerList<ICompoundEditListener> fListenerList= new ListenerList<>(ListenerList.IDENTITY);

	private ITextViewer fViewer;
	private StyledText fWidgetEventSource;

	/**
	 * Creates a new strategy, equivalent to calling
	 * {@linkplain #CompoundEditExitStrategy(String[]) CompoundEditExitStrategy(new String[] &#x7b; commandId &#x7d;)}.
	 *
	 * @param commandId the command id of the repeatable command
	 */
	public CompoundEditExitStrategy(String commandId) {
		if (commandId == null)
			throw new NullPointerException("commandId"); //$NON-NLS-1$
		fCommandIds= new String[] {commandId};
	}

	/**
	 * Creates a new strategy, ending upon execution of any command other than the ones
	 * specified.
	 *
	 * @param commandIds the ids of the repeatable commands
	 */
	public CompoundEditExitStrategy(String[] commandIds) {
		for (int i= 0; i < commandIds.length; i++) {
			if (commandIds[i] == null)
				throw new NullPointerException("commandIds[" + i + "]"); //$NON-NLS-1$ //$NON-NLS-2$
		}
		fCommandIds= new String[commandIds.length];
		System.arraycopy(commandIds, 0, fCommandIds, 0, commandIds.length);
	}

	/**
	 * Installs the receiver on <code>viewer</code> and arms it. After this call returns, any
	 * registered listeners will be notified if a compound edit ends.
	 *
	 * @param viewer the viewer to install on
	 */
	public void arm(ITextViewer viewer) {
		disarm();
		if (viewer == null)
			throw new NullPointerException("editor"); //$NON-NLS-1$
		fViewer= viewer;
		addListeners(fViewer);
	}

	/**
	 * Disarms the receiver. After this call returns, any registered listeners will be not be
	 * notified any more until <code>install</code> is called again. Note that the listeners are
	 * not removed.
	 * <p>
	 * Note that the receiver is automatically disarmed when the end of a compound edit has
	 * been detected and before the listeners are notified.
	 * </p>
	 */
	public void disarm() {
		if (isInstalled()) {
			removeListeners();
			fViewer= null;
		}
	}

	private void addListeners(ITextViewer viewer) {
		fWidgetEventSource= viewer.getTextWidget();
		if (fWidgetEventSource != null) {
			fWidgetEventSource.addVerifyKeyListener(fEventListener);
			fWidgetEventSource.addMouseListener(fEventListener);
			fWidgetEventSource.addFocusListener(fEventListener);
		}

		ICommandService commandService= PlatformUI.getWorkbench().getAdapter(ICommandService.class);
		if (commandService != null)
			commandService.addExecutionListener(fEventListener);
	}

	private void removeListeners() {
		ICommandService commandService= PlatformUI.getWorkbench().getAdapter(ICommandService.class);
		if (commandService != null)
			commandService.removeExecutionListener(fEventListener);

		if (fWidgetEventSource != null) {
			fWidgetEventSource.removeFocusListener(fEventListener);
			fWidgetEventSource.removeMouseListener(fEventListener);
			fWidgetEventSource.removeVerifyKeyListener(fEventListener);
			fWidgetEventSource= null;
		}
	}

	private boolean isInstalled() {
		return fViewer != null;
	}

	private void fireEndCompoundEdit() {
		disarm();
		for (ICompoundEditListener listener : fListenerList) {
			try {
				listener.endCompoundEdit();
			} catch (Exception e) {
				IStatus status= new Status(IStatus.ERROR, TextEditorPlugin.PLUGIN_ID, IStatus.OK, "listener notification failed", e); //$NON-NLS-1$
				TextEditorPlugin.getDefault().getLog().log(status);
			}
		}
	}

	/**
	 * Adds a compound edit listener. Multiple registration is possible. Note that the receiver is
	 * automatically disarmed before the listeners are notified.
	 *
	 * @param listener the new listener
	 */
	public void addCompoundListener(ICompoundEditListener listener) {
		fListenerList.add(listener);
	}

	/**
	 * Removes a compound edit listener. If <code>listener</code> is registered multiple times, an
	 * arbitrary instance is removed. If <code>listener</code> is not currently registered,
	 * nothing happens.
	 *
	 * @param listener the listener to be removed.
	 */
	public void removeCompoundListener(ICompoundEditListener listener) {
		fListenerList.remove(listener);
	}

}

Back to the top