Source code: org/eclipse/ui/part/MultiPageEditorPart.java
1 /*******************************************************************************
2 * Copyright (c) 2000, 2004 IBM Corporation and others.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Common Public License v1.0
5 * which accompanies this distribution, and is available at
6 * http://www.eclipse.org/legal/cpl-v10.html
7 *
8 * Contributors:
9 * IBM Corporation - initial API and implementation
10 *******************************************************************************/
11 package org.eclipse.ui.part;
12
13
14 import java.util.ArrayList;
15 import java.util.Iterator;
16
17 import org.eclipse.core.runtime.Platform;
18
19 import org.eclipse.swt.SWT;
20 import org.eclipse.swt.custom.CTabFolder;
21 import org.eclipse.swt.custom.CTabItem;
22 import org.eclipse.swt.events.*;
23 import org.eclipse.swt.graphics.Image;
24 import org.eclipse.swt.layout.FillLayout;
25 import org.eclipse.swt.widgets.Composite;
26 import org.eclipse.swt.widgets.Control;
27 import org.eclipse.swt.widgets.Item;
28
29 import org.eclipse.jface.util.Assert;
30 import org.eclipse.jface.util.SafeRunnable;
31 import org.eclipse.jface.viewers.ISelectionProvider;
32 import org.eclipse.jface.viewers.SelectionChangedEvent;
33
34 import org.eclipse.ui.IEditorActionBarContributor;
35 import org.eclipse.ui.IEditorInput;
36 import org.eclipse.ui.IEditorPart;
37 import org.eclipse.ui.IEditorSite;
38 import org.eclipse.ui.IKeyBindingService;
39 import org.eclipse.ui.INestableKeyBindingService;
40 import org.eclipse.ui.IPropertyListener;
41 import org.eclipse.ui.IWorkbenchPart;
42 import org.eclipse.ui.PartInitException;
43
44 import org.eclipse.ui.internal.WorkbenchPlugin;
45
46 /**
47 * A multi-page editor is an editor with multiple pages, each of which may
48 * contain an editor or an arbitrary SWT control.
49 * <p>
50 * Subclasses must implement the following methods:
51 * <ul>
52 * <li><code>createPages</code> - to create the required pages by calling one
53 * of the <code>addPage</code> methods</li>
54 * <li><code>IEditorPart.doSave</code> - to save contents of editor</li>
55 * <li><code>IEditorPart.doSaveAs</code> - to save contents of editor</li>
56 * <li><code>IEditorPart.isSaveAsAllowed</code> - to enable Save As</li>
57 * <li><code>IEditorPart.gotoMarker</code> - to scroll to a marker</li>
58 * </ul>
59 * </p>
60 * <p>
61 * Multi-page editors have a single action bar contributor, which manages
62 * contributions for all the pages. The contributor must be a subclass of
63 * <code>AbstractMultiPageEditorActionBarContributor</code>.
64 * Note that since any nested editors are created directly in code by callers of
65 * <code>addPage(IEditorPart,IEditorInput)</code>, nested editors do not have
66 * their own contributors.
67 * </p>
68 *
69 * @see org.eclipse.ui.part.MultiPageEditorActionBarContributor
70 */
71 public abstract class MultiPageEditorPart extends EditorPart {
72 /**
73 * The container widget.
74 */
75 private CTabFolder container;
76
77 /**
78 * List of nested editors. Element type: IEditorPart.
79 * Need to hang onto them here, in addition to using get/setData on the items,
80 * because dispose() needs to access them, but widgetry has already been disposed at that point.
81 */
82 private ArrayList nestedEditors = new ArrayList(3);
83 /**
84 * Creates an empty multi-page editor with no pages.
85 */
86 protected MultiPageEditorPart() {
87 super();
88 }
89 /**
90 * Creates and adds a new page containing the given control to this multi-page
91 * editor. The control may be <code>null</code>, allowing it to be created
92 * and set later using <code>setControl</code>.
93 *
94 * @param control the control, or <code>null</code>
95 * @return the index of the new page
96 *
97 * @see MultiPageEditorPart#setControl(int, Control)
98 */
99 public int addPage(Control control) {
100 createItem(control);
101 return getPageCount() - 1;
102 }
103 /**
104 * Creates and adds a new page containing the given editor to this multi-page
105 * editor. This also hooks a property change listener on the nested editor.
106 *
107 * @param editor the nested editor
108 * @param input the input for the nested editor
109 * @return the index of the new page
110 * @exception PartInitException if a new page could not be created
111 *
112 * @see MultiPageEditorPart#handlePropertyChange(int) the handler for property change events from the nested editor
113 */
114 public int addPage(IEditorPart editor, IEditorInput input) throws PartInitException {
115 IEditorSite site = createSite(editor);
116 // call init first so that if an exception is thrown, we have created no new widgets
117 editor.init(site, input);
118 Composite parent2 = new Composite(getContainer(), SWT.NONE);
119 parent2.setLayout(new FillLayout());
120 editor.createPartControl(parent2);
121 editor.addPropertyListener(
122 new IPropertyListener() {
123 public void propertyChanged(Object source, int propertyId) {
124 MultiPageEditorPart.this.handlePropertyChange(propertyId);
125 }
126 });
127 // create item for page only after createPartControl has succeeded
128 Item item = createItem(parent2);
129 // remember the editor, as both data on the item, and in the list of editors (see field comment)
130 item.setData(editor);
131 nestedEditors.add(editor);
132 return getPageCount() - 1;
133 }
134 /**
135 * Creates an empty container. Creates a CTabFolder with no style bits set, and
136 * hooks a selection listener which calls <code>pageChange()</code> whenever
137 * the selected tab changes.
138 *
139 * @param parent
140 * The composite in which the container tab folder should be
141 * created; must not be <code>null</code>.
142 * @return a new container
143 */
144 private CTabFolder createContainer(Composite parent) {
145 // use SWT.FLAT style so that an extra 1 pixel border is not reserved
146 // inside the folder
147 final CTabFolder newContainer = new CTabFolder(parent, SWT.BOTTOM | SWT.FLAT);
148 newContainer.addSelectionListener(new SelectionAdapter() {
149 public void widgetSelected(SelectionEvent e) {
150 int newPageIndex = newContainer.indexOf((CTabItem) e.item);
151 pageChange(newPageIndex);
152 }
153 });
154 return newContainer;
155 }
156 /**
157 * Creates a tab item and places control in the new item.
158 * The item is a CTabItem with no style bits set.
159 *
160 * @param control is the control to be placed in an item
161 * @return a new item
162 */
163 private CTabItem createItem(Control control) {
164 CTabItem item = new CTabItem(getTabFolder(), SWT.NONE);
165 item.setControl(control);
166 return item;
167 }
168 /**
169 * Creates the pages of this multi-page editor.
170 * <p>
171 * Subclasses must implement this method.
172 * </p>
173 */
174 protected abstract void createPages();
175 /**
176 * The <code>MultiPageEditor</code> implementation of this <code>IWorkbenchPart</code>
177 * method creates the control for the multi-page editor by calling <code>createContainer</code>,
178 * then <code>createPages</code>. Subclasses should implement <code>createPages</code>
179 * rather than overriding this method.
180 *
181 * @param parent
182 * The parent in which the editor should be created; must not be
183 * <code>null</code>.
184 */
185 public final void createPartControl(Composite parent) {
186 this.container = createContainer(parent);
187 createPages();
188 // set the active page (page 0 by default), unless it has already been done
189 if (getActivePage() == -1)
190 setActivePage(0);
191 }
192 /**
193 * Creates the site for the given nested editor.
194 * The <code>MultiPageEditorPart</code> implementation of this method creates an
195 * instance of <code>MultiPageEditorSite</code>. Subclasses may reimplement
196 * to create more specialized sites.
197 *
198 * @param editor the nested editor
199 * @return the editor site
200 */
201 protected IEditorSite createSite(IEditorPart editor){
202 return new MultiPageEditorSite(this, editor);
203 }
204 /**
205 * The <code>MultiPageEditorPart</code> implementation of this
206 * <code>IWorkbenchPart</code> method disposes all nested editors.
207 * Subclasses may extend.
208 */
209 public void dispose() {
210 for (int i = 0; i < nestedEditors.size(); ++i) {
211 IEditorPart editor = (IEditorPart) nestedEditors.get(i);
212 disposePart(editor);
213 }
214 nestedEditors.clear();
215 }
216 /**
217 * Returns the active nested editor if there is one.
218 * <p>
219 * Subclasses should not override this method
220 * </p>
221 *
222 * @return the active nested editor, or <code>null</code> if none
223 */
224 protected IEditorPart getActiveEditor() {
225 int index = getActivePage();
226 if (index != -1)
227 return getEditor(index);
228 return null;
229 }
230 /**
231 * Returns the index of the currently active page,
232 * or -1 if there is no active page.
233 * <p>
234 * Subclasses should not override this method
235 * </p>
236 *
237 * @return the index of the active page, or -1 if there is no active page
238 */
239 protected int getActivePage() {
240 CTabFolder tabFolder = getTabFolder();
241 if (tabFolder != null && !tabFolder.isDisposed())
242 return tabFolder.getSelectionIndex();
243 return -1;
244 }
245 /**
246 * Returns the composite control containing this multi-page editor's pages.
247 * This should be used as the parent when creating controls for the individual pages.
248 * That is, when calling <code>addPage(Control)</code>, the passed control should be
249 * a child of this container.
250 * <p>
251 * Warning: Clients should not assume that the container is any particular subclass
252 * of Composite. The actual class used may change in order to improve the look and feel of
253 * multi-page editors. Any code making assumptions on the particular subclass would thus be broken.
254 * </p>
255 * <p>
256 * Subclasses should not override this method
257 * </p>
258 *
259 * @return the composite, or <code>null</code> if <code>createPartControl</code>
260 * has not been called yet
261 */
262 protected Composite getContainer() {
263 return container;
264 }
265 /**
266 * Returns the control for the given page index, or <code>null</code>
267 * if no control has been set for the page.
268 * The page index must be valid.
269 * <p>
270 * Subclasses should not override this method
271 * </p>
272 *
273 * @param pageIndex the index of the page
274 * @return the control for the specified page, or <code>null</code> if none has been set
275 */
276 protected Control getControl(int pageIndex) {
277 return getItem(pageIndex).getControl();
278 }
279 /**
280 * Returns the editor for the given page index.
281 * The page index must be valid.
282 *
283 * @param pageIndex the index of the page
284 * @return the editor for the specified page, or <code>null</code> if the
285 * specified page was not created with
286 * <code>addPage(IEditorPart,IEditorInput)</code>
287 */
288 protected IEditorPart getEditor(int pageIndex) {
289 Item item = getItem(pageIndex);
290 if (item != null) {
291 Object data = item.getData();
292 if (data instanceof IEditorPart) {
293 return (IEditorPart) data;
294 }
295 }
296 return null;
297 }
298 /**
299 * Returns the tab item for the given page index (page index is 0-based).
300 * The page index must be valid.
301 *
302 * @param pageIndex the index of the page
303 * @return the tab item for the given page index
304 */
305 private CTabItem getItem(int pageIndex) {
306 return getTabFolder().getItem(pageIndex);
307 }
308 /**
309 * Returns the number of pages in this multi-page editor.
310 *
311 * @return the number of pages
312 */
313 protected int getPageCount() {
314 CTabFolder folder = getTabFolder();
315 // May not have been created yet, or may have been disposed.
316 if (folder != null && !folder.isDisposed())
317 return folder.getItemCount();
318 return 0;
319 }
320 /**
321 * Returns the image for the page with the given index,
322 * or <code>null</code> if no image has been set for the page.
323 * The page index must be valid.
324 *
325 * @param pageIndex the index of the page
326 * @return the image, or <code>null</code> if none
327 */
328 protected Image getPageImage(int pageIndex) {
329 return getItem(pageIndex).getImage();
330 }
331 /**
332 * Returns the text label for the page with the given index.
333 * Returns the empty string if no text label has been set for the page.
334 * The page index must be valid.
335 *
336 * @param pageIndex the index of the page
337 * @return the text label for the page
338 */
339 protected String getPageText(int pageIndex) {
340 return getItem(pageIndex).getText();
341 }
342 /**
343 * Returns the tab folder containing this multi-page editor's pages.
344 *
345 * @return the tab folder, or <code>null</code> if <code>createPartControl</code>
346 * has not been called yet
347 */
348 private CTabFolder getTabFolder() {
349 return container;
350 }
351 /**
352 * Handles a property change notification from a nested editor.
353 * The default implementation simply forwards the change to listeners
354 * on this multi-page editor by calling <code>firePropertyChange</code>
355 * with the same property id. For example, if the dirty state of a nested editor
356 * changes (property id <code>IEditorPart.PROP_DIRTY</code>), this method
357 * handles it by firing a property change event for <code>IEditorPart.PROP_DIRTY</code>
358 * to property listeners on this multi-page editor.
359 * <p>
360 * Subclasses may extend or reimplement this method.
361 * </p>
362 *
363 * @param propertyId the id of the property that changed
364 */
365 protected void handlePropertyChange (int propertyId) {
366 firePropertyChange(propertyId);
367 }
368 /**
369 * The <code>MultiPageEditorPart</code> implementation of this <code>IEditorPart</code>
370 * method sets its site to the given site, its input to the given input, and
371 * the site's selection provider to a <code>MultiPageSelectionProvider</code>.
372 * Subclasses may extend this method.
373 *
374 * @param site
375 * The site for which this part is being created; must not be <code>null</code>.
376 * @param input
377 * The input on which this editor should be created; must not be
378 * <code>null</code>.
379 * @throws PartInitException
380 * If the initialization of the part fails -- currently never.
381 */
382 public void init(IEditorSite site, IEditorInput input) throws PartInitException {
383 setSite(site);
384 setInput(input);
385 site.setSelectionProvider(new MultiPageSelectionProvider(this));
386 }
387 /**
388 * The <code>MultiPageEditorPart</code> implementation of this <code>IEditorPart</code>
389 * method returns whether the contents of any of this multi-page editor's
390 * nested editors have changed since the last save. Pages created with <code>addPage(Control)</code>
391 * are ignored.
392 * <p>
393 * Subclasses may extend or reimplement this method.
394 * </p>
395 *
396 * @return <code>true</code> if any of the nested editors are dirty; <code>false</code>
397 * otherwise.
398 */
399 public boolean isDirty() {
400 // use nestedEditors to avoid SWT requests; see bug 12996
401 for (Iterator i = nestedEditors.iterator(); i.hasNext();) {
402 IEditorPart editor = (IEditorPart) i.next();
403 if (editor.isDirty()) {
404 return true;
405 }
406 }
407 return false;
408 }
409 /**
410 * Notifies this multi-page editor that the page with the given id has been
411 * activated. This method is called when the user selects a different tab.
412 * <p>
413 * The <code>MultiPageEditorPart</code> implementation of this method
414 * sets focus to the new page, and notifies the action bar contributor (if there is one).
415 * This checks whether the action bar contributor is an instance of
416 * <code>MultiPageEditorActionBarContributor</code>, and, if so,
417 * calls <code>setActivePage</code> with the active nested editor.
418 * This also fires a selection change event if required.
419 * </p>
420 * <p>
421 * Subclasses may extend this method.
422 * </p>
423 *
424 * @param newPageIndex the index of the activated page
425 */
426 protected void pageChange(int newPageIndex) {
427 // XXX: Workaround for 1GCN531: SWT:WIN2000 - CTabFolder child's visibility is false on notification
428 Control control = getControl(newPageIndex);
429 if (control != null) {
430 control.setVisible(true);
431 }
432 // XXX: End workaround
433 setFocus();
434 IEditorPart activeEditor = getEditor(newPageIndex);
435 IEditorActionBarContributor contributor = getEditorSite().getActionBarContributor();
436 if (contributor != null && contributor instanceof MultiPageEditorActionBarContributor) {
437 ((MultiPageEditorActionBarContributor) contributor).setActivePage(activeEditor);
438 }
439 if (activeEditor != null) {
440 //Workaround for 1GAUS7C: ITPUI:ALL - Editor not activated when restored from previous session
441 //do not need second if once fixed
442 ISelectionProvider selectionProvider = activeEditor.getSite().getSelectionProvider();
443 if (selectionProvider != null) {
444 SelectionChangedEvent event = new SelectionChangedEvent(selectionProvider, selectionProvider.getSelection());
445 ((MultiPageSelectionProvider) getSite().getSelectionProvider()).fireSelectionChanged(event);
446 }
447 }
448 }
449 /**
450 * Disposes the given part and its site.
451 *
452 * @param part
453 * The part to dispose; must not be <code>null</code>.
454 */
455 private void disposePart(final IWorkbenchPart part) {
456 Platform.run(new SafeRunnable() {
457 public void run() {
458 if (part.getSite() instanceof MultiPageEditorSite) {
459 MultiPageEditorSite partSite = (MultiPageEditorSite) part.getSite();
460 partSite.dispose();
461 }
462 part.dispose();
463 }
464 public void handleException(Throwable e) {
465 //Exception has already being logged by Core. Do nothing.
466 }
467 });
468 }
469 /**
470 * Removes the page with the given index from this multi-page editor. The
471 * controls for the page are disposed of; if the page has an editor, it is
472 * disposed of too. The page index must be valid.
473 *
474 * @param pageIndex
475 * the index of the page
476 * @see MultiPageEditorPart#addPage(Control)
477 * @see MultiPageEditorPart#addPage(IEditorPart, IEditorInput)
478 */
479 public void removePage(int pageIndex) {
480 Assert.isTrue(pageIndex >= 0 && pageIndex < getPageCount());
481 // get editor (if any) before disposing item
482 IEditorPart editor = getEditor(pageIndex);
483 // dispose item before disposing editor, in case there's an exception in editor's dispose
484 getItem(pageIndex).dispose();
485 // dispose editor (if any)
486 if (editor != null) {
487 nestedEditors.remove(editor);
488 disposePart(editor);
489 }
490 }
491 /**
492 * Sets the currently active page.
493 *
494 * @param pageIndex the index of the page to be activated; the index must be valid
495 */
496 protected void setActivePage(int pageIndex) {
497 Assert.isTrue(pageIndex >= 0 && pageIndex < getPageCount());
498 getTabFolder().setSelection(pageIndex);
499 }
500 /**
501 * Sets the control for the given page index.
502 * The page index must be valid.
503 *
504 * @param pageIndex the index of the page
505 * @param control the control for the specified page, or <code>null</code> to clear the control
506 */
507 protected void setControl(int pageIndex, Control control) {
508 getItem(pageIndex).setControl(control);
509 }
510 /**
511 * The <code>MultiPageEditor</code> implementation of this
512 * <code>IWorkbenchPart</code> method sets focus on the active nested editor,
513 * if there is one.
514 * <p>
515 * Subclasses may extend or reimplement.
516 * </p>
517 */
518 public void setFocus() {
519 setFocus(getActivePage());
520 }
521 /**
522 * Sets focus to the control for the given page. If the page has an editor,
523 * this calls its <code>setFocus()</code> method. Otherwise, this calls
524 * <code>setFocus</code> on the control for the page.
525 *
526 * @param pageIndex
527 * the index of the page
528 */
529 private void setFocus(int pageIndex) {
530 final IKeyBindingService service = getSite().getKeyBindingService();
531 if (pageIndex < 0 || pageIndex >= getPageCount()) {
532 // There is no selected page, so deactivate the active service.
533 if (service instanceof INestableKeyBindingService) {
534 final INestableKeyBindingService nestableService = (INestableKeyBindingService) service;
535 nestableService.activateKeyBindingService(null);
536 } else {
537 WorkbenchPlugin
538 .log("MultiPageEditorPart.setFocus() Parent key binding service was not an instance of INestableKeyBindingService. It was an instance of " + service.getClass().getName() + " instead."); //$NON-NLS-1$ //$NON-NLS-2$
539 }
540 return;
541 }
542
543 final IEditorPart editor = getEditor(pageIndex);
544 if (editor != null) {
545 editor.setFocus();
546 // There is no selected page, so deactivate the active service.
547 if (service instanceof INestableKeyBindingService) {
548 final INestableKeyBindingService nestableService = (INestableKeyBindingService) service;
549 if (editor != null) {
550 nestableService.activateKeyBindingService(editor
551 .getEditorSite());
552 } else {
553 nestableService.activateKeyBindingService(null);
554 }
555 } else {
556 WorkbenchPlugin
557 .log("MultiPageEditorPart.setFocus() Parent key binding service was not an instance of INestableKeyBindingService. It was an instance of " + service.getClass().getName() + " instead."); //$NON-NLS-1$ //$NON-NLS-2$
558 }
559 } else {
560 // There is no selected editor, so deactivate the active service.
561 if (service instanceof INestableKeyBindingService) {
562 final INestableKeyBindingService nestableService = (INestableKeyBindingService) service;
563 nestableService.activateKeyBindingService(null);
564 } else {
565 WorkbenchPlugin
566 .log("MultiPageEditorPart.setFocus() Parent key binding service was not an instance of INestableKeyBindingService. It was an instance of " + service.getClass().getName() + " instead."); //$NON-NLS-1$ //$NON-NLS-2$
567 }
568
569 // Give the page's control focus.
570 final Control control = getControl(pageIndex);
571 if (control != null) {
572 control.setFocus();
573 }
574 }
575 }
576 /**
577 * Sets the image for the page with the given index, or <code>null</code>
578 * to clear the image for the page.
579 * The page index must be valid.
580 *
581 * @param pageIndex the index of the page
582 * @param image the image, or <code>null</code>
583 */
584 protected void setPageImage(int pageIndex, Image image) {
585 getItem(pageIndex).setImage(image);
586 }
587 /**
588 * Sets the text label for the page with the given index.
589 * The page index must be valid.
590 * The text label must not be null.
591 *
592 * @param pageIndex the index of the page
593 * @param text the text label
594 */
595 protected void setPageText(int pageIndex, String text) {
596 getItem(pageIndex).setText(text);
597 }
598 }