/*******************************************************************************
* Copyright (c) 2011 Wind River Systems, Inc. and others. 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:
* Wind River Systems - initial API and implementation
*******************************************************************************/
package org.eclipse.tm.te.ui.terminals.tabs;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.EventObject;
import java.util.Iterator;
import java.util.List;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.PlatformObject;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CTabFolder;
import org.eclipse.swt.custom.CTabItem;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.tm.internal.terminal.control.ITerminalListener;
import org.eclipse.tm.internal.terminal.control.ITerminalViewControl;
import org.eclipse.tm.internal.terminal.control.TerminalViewControlFactory;
import org.eclipse.tm.internal.terminal.provisional.api.ITerminalConnector;
import org.eclipse.tm.internal.terminal.provisional.api.TerminalState;
import org.eclipse.tm.te.runtime.events.EventManager;
import org.eclipse.tm.te.ui.events.AbstractEventListener;
import org.eclipse.tm.te.ui.swt.DisplayUtil;
import org.eclipse.tm.te.ui.terminals.activator.UIPlugin;
import org.eclipse.tm.te.ui.terminals.events.SelectionChangedBroadcastEvent;
import org.eclipse.tm.te.ui.terminals.interfaces.ITerminalsView;
import org.eclipse.tm.te.ui.terminals.interfaces.ImageConsts;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.ide.IDEEncoding;
/**
* Terminals tab folder manager.
*/
@SuppressWarnings("restriction")
public class TabFolderManager extends PlatformObject implements ISelectionProvider {
// Reference to the parent terminal consoles view
private final ITerminalsView parentView;
// Reference to the selection listener instance
private final SelectionListener selectionListener;
// Reference to the broadcasted selection changed event listener instance
private final BroadcastedSelectionChangedEventListener broadcastedSelectionChangedEventListener;
/**
* List of selection changed listeners.
*/
private final List<ISelectionChangedListener> selectionChangedListeners = new ArrayList<ISelectionChangedListener>();
/**
* The terminal control selection listener implementation.
*/
private class TerminalControlSelectionListener implements DisposeListener, MouseListener {
private final ITerminalViewControl terminal;
private boolean selectMode;
/**
* Constructor.
*
* @param terminal The terminal control. Must be not <code>null</code>.
*/
public TerminalControlSelectionListener(ITerminalViewControl terminal) {
Assert.isNotNull(terminal);
this.terminal = terminal;
// Register ourself as the required listener
terminal.getControl().addDisposeListener(this);
terminal.getControl().addMouseListener(this);
}
/**
* Returns the associated terminal view control.
*
* @return The terminal view control.
*/
protected final ITerminalViewControl getTerminal() {
return terminal;
}
/* (non-Javadoc)
* @see org.eclipse.swt.events.DisposeListener#widgetDisposed(org.eclipse.swt.events.DisposeEvent)
*/
@Override
public void widgetDisposed(DisposeEvent e) {
// Widget got disposed, check if it is ours
// If a tab item gets disposed, we have to dispose the terminal as well
if (e.getSource().equals(terminal.getControl())) {
// Remove as listener
getTerminal().getControl().removeDisposeListener(this);
getTerminal().getControl().removeMouseListener(this);
}
}
/* (non-Javadoc)
* @see org.eclipse.swt.events.MouseListener#mouseDown(org.eclipse.swt.events.MouseEvent)
*/
@Override
public void mouseDown(MouseEvent e) {
// Left button down -> select mode starts
if (e.button == 1) selectMode = true;
}
/* (non-Javadoc)
* @see org.eclipse.swt.events.MouseListener#mouseUp(org.eclipse.swt.events.MouseEvent)
*/
@Override
public void mouseUp(MouseEvent e) {
if (e.button == 1 && selectMode) {
selectMode = false;
// Fire a selection changed event with the terminal controls selection
DisplayUtil.safeAsyncExec(new Runnable() {
@Override
public void run() {
fireSelectionChanged(new StructuredSelection(getTerminal().getSelection()));
}
});
}
}
/* (non-Javadoc)
* @see org.eclipse.swt.events.MouseListener#mouseDoubleClick(org.eclipse.swt.events.MouseEvent)
*/
@Override
public void mouseDoubleClick(MouseEvent e) {
}
}
/**
* The event listener to process broadcasted selection changed events
*/
private class BroadcastedSelectionChangedEventListener extends AbstractEventListener {
private final TabFolderManager parent;
/**
* Constructor.
*
* @param parent The parent tab folder manager. Must not be <code>null</code>.
*/
public BroadcastedSelectionChangedEventListener(TabFolderManager parent) {
super();
Assert.isNotNull(parent);
this.parent = parent;
}
/* (non-Javadoc)
* @see org.eclipse.tm.te.runtime.interfaces.events.IEventListener#eventFired(java.util.EventObject)
*/
@Override
public void eventFired(EventObject event) {
if (event instanceof SelectionChangedBroadcastEvent && !event.getSource().equals(parent)) {
// Don't need to do anything if the parent tab folder is disposed or does not have a open tab
CTabFolder tabFolder = parent.getTabFolder();
if (tabFolder == null || tabFolder.isDisposed() || tabFolder.getItemCount() == 0) return;
// Received a broadcasted selection changed event from another tab folder manager.
SelectionChangedEvent selectionChangedEvent = ((SelectionChangedBroadcastEvent)event).getSelectionChangedEvent();
if (selectionChangedEvent != null && selectionChangedEvent.getSelection() instanceof IStructuredSelection && !selectionChangedEvent.getSelection().isEmpty()) {
// Extract the selection from the selection changed event
IStructuredSelection selection = (IStructuredSelection)selectionChangedEvent.getSelection();
// Determine the first element in the selection being a CTabItem
CTabItem item = null;
Iterator<?> iterator = selection.iterator();
while (iterator.hasNext()) {
Object candidate = iterator.next();
if (candidate instanceof CTabItem) { item = (CTabItem)candidate; break; }
}
// If we got an CTabItem from the selection, try to find a CTabItem in our own tab folder manager
// which is associated with the exact same data object.
if (item != null && item.getData("customData") != null) { //$NON-NLS-1$
Object data = item.getData("customData"); //$NON-NLS-1$
CTabItem[] ourItems = tabFolder.getItems();
for (CTabItem ourItem : ourItems) {
Object ourData = ourItem.getData("customData"); //$NON-NLS-1$
if (data.equals(ourData) && !ourItem.equals(parent.getActiveTabItem())) {
// Select this item and we are done
parent.setSelection(new StructuredSelection(ourItem));
break;
}
}
}
}
}
}
}
/**
* Constructor.
*
* @param parentView The parent terminal console view. Must be not <code>null</code>.
*/
public TabFolderManager(ITerminalsView parentView) {
super();
Assert.isNotNull(parentView);
this.parentView = parentView;
// Attach a selection listener to the tab folder
selectionListener = doCreateTabFolderSelectionListener(this);
if (getTabFolder() != null) getTabFolder().addSelectionListener(selectionListener);
// Create and register the broadcasted selection changed event listener
broadcastedSelectionChangedEventListener = doCreateBroadcastedSelectionChangedEventListener(this);
if (isListeningToBroadcastedSelectionChangedEvent() && broadcastedSelectionChangedEventListener != null) {
EventManager.getInstance().addEventListener(broadcastedSelectionChangedEventListener, SelectionChangedBroadcastEvent.class);
}
}
/**
* Creates the terminal console tab folder selection listener instance.
*
* @param parent The parent terminal console tab folder manager. Must be not <code>null</code>.
* @return The selection listener instance.
*/
protected TabFolderSelectionListener doCreateTabFolderSelectionListener(TabFolderManager parent) {
Assert.isNotNull(parent);
return new TabFolderSelectionListener(parent);
}
/**
* Returns the parent terminal consoles view.
*
* @return The terminal consoles view instance.
*/
protected final ITerminalsView getParentView() {
return parentView;
}
/**
* Returns the tab folder associated with the parent view.
*
* @return The tab folder or <code>null</code>.
*/
protected final CTabFolder getTabFolder() {
return (CTabFolder)getParentView().getAdapter(CTabFolder.class);
}
/**
* Returns the selection changed listeners currently registered.
*
* @return The registered selection changed listeners or an empty array.
*/
protected final ISelectionChangedListener[] getSelectionChangedListeners() {
return selectionChangedListeners.toArray(new ISelectionChangedListener[selectionChangedListeners.size()]);
}
/**
* Dispose the tab folder manager instance.
*/
public void dispose() {
// Dispose the selection listener
if (getTabFolder() != null && !getTabFolder().isDisposed()) getTabFolder().removeSelectionListener(selectionListener);
// Remove the broadcasted selection changed event listener from the notification manager
if (broadcastedSelectionChangedEventListener != null) {
EventManager.getInstance().removeEventListener(broadcastedSelectionChangedEventListener);
}
}
/**
* Creates a new tab item with the given title and connector.
*
* @param title The tab title. Must be not <code>null</code>.
* @param connector The terminal connector. Must be not <code>null</code>.
* @param data The custom terminal data node or <code>null</code>.
*
* @return The created tab item or <code>null</code> if failed.
*/
@SuppressWarnings("unused")
public CTabItem createTabItem(String title, ITerminalConnector connector, Object data) {
Assert.isNotNull(title);
Assert.isNotNull(connector);
// The result tab item
CTabItem item = null;
// Get the tab folder from the parent viewer
CTabFolder tabFolder = getTabFolder();
if (tabFolder != null) {
// Generate a unique title string for the new tab item (must be called before creating the item itself)
title = makeUniqueTitle(title, tabFolder);
// Create the tab item
item = new CTabItem(tabFolder, SWT.CLOSE);
// Set the tab item title
item.setText(title);
// Set the tab icon
Image image = getTabItemImage(connector, data);
if (image != null) item.setImage(image);
// Setup the tab item listeners
setupTerminalTabListeners(item);
// Create the composite to create the terminal control within
Composite composite = new Composite(tabFolder, SWT.NONE);
composite.setLayout(new FillLayout());
// Associate the composite with the tab item
item.setControl(composite);
// Refresh the layout
tabFolder.getParent().layout(true);
// Create the terminal control
ITerminalViewControl terminal = TerminalViewControlFactory.makeControl(doCreateTerminalTabTerminalListener(item), composite, new ITerminalConnector[] { connector });
// Add the "selection" listener to the terminal control
new TerminalControlSelectionListener(terminal);
// Use the default Eclipse IDE encoding setting to configure the terminals encoding
try { terminal.setEncoding(IDEEncoding.getResourceEncoding()); } catch (UnsupportedEncodingException e) { /* ignored on purpose */ }
// Associated the terminal with the tab item
item.setData(terminal);
// Associated the custom data node with the tab item (if any)
if (data != null) item.setData("customData", data); //$NON-NLS-1$
// Overwrite the text canvas help id
String contextHelpId = getParentView().getContextHelpId();
if (contextHelpId != null) {
PlatformUI.getWorkbench().getHelpSystem().setHelp(terminal.getControl(), contextHelpId);
}
// Set the context menu
TabFolderMenuHandler menuHandler = (TabFolderMenuHandler)getParentView().getAdapter(TabFolderMenuHandler.class);
if (menuHandler != null) {
Menu menu = (Menu)menuHandler.getAdapter(Menu.class);
if (menu != null) {
// One weird occurrence of IllegalArgumentException: Widget has wrong parent.
// Inspecting the code, this seem extremely unlikely. The terminal is created
// from a composite parent, the composite parent from the tab folder and the menu
// from the tab folder. Means, at the end all should have the same menu shell, shouldn't they?
try {
terminal.getControl().setMenu(menu);
} catch (IllegalArgumentException e) {
// Log exception only if debug mode is set to 1.
if (UIPlugin.getTraceHandler().isSlotEnabled(1, null)) {
e.printStackTrace();
}
}
}
}
// Select the created item within the tab folder
tabFolder.setSelection(item);
// Set the connector
terminal.setConnector(connector);
// And connect the terminal
terminal.connectTerminal();
// Fire selection changed event
fireSelectionChanged();
}
// Return the create tab item finally.
return item;
}
/**
* Generate a unique title string based on the given proposal.
*
* @param proposal The proposal. Must be not <code>null</code>.
* @return The unique title string.
*/
protected String makeUniqueTitle(String proposal, CTabFolder tabFolder) {
Assert.isNotNull(proposal);
Assert.isNotNull(tabFolder);
String title = proposal;
int index = 0;
// Loop all existing tab items and check the titles. We have to remember
// all found titles as modifying the proposal might in turn conflict again
// with the title of a tab already checked.
List<String> titles = new ArrayList<String>();
for (CTabItem item : tabFolder.getItems()) {
// Get the tab item title
titles.add(item.getText());
// Make the proposal unique be appending (<n>) against all known titles.
while (titles.contains(title)) title = proposal + " (" + index++ + ")"; //$NON-NLS-1$ //$NON-NLS-2$
}
return title;
}
/**
* Setup the terminal console tab item listeners.
*
* @param item The tab item. Must be not <code>null</code>.
*/
protected void setupTerminalTabListeners(CTabItem item) {
Assert.isNotNull(item);
// Create and associate the disposal listener
item.addDisposeListener(doCreateTerminalTabDisposeListener(this));
// Create and register the property change listener
final IPropertyChangeListener propertyChangeListener = doCreateTerminalTabPropertyChangeListener(item);
// Register to the JFace font registry
JFaceResources.getFontRegistry().addListener(propertyChangeListener);
// Remove the listener from the JFace font registry if the tab gets disposed
item.addDisposeListener(new DisposeListener() {
@Override
public void widgetDisposed(DisposeEvent e) {
JFaceResources.getFontRegistry().removeListener(propertyChangeListener);
}
});
}
/**
* Creates a new terminal console tab terminal listener instance.
*
* @param item The tab item. Must be not <code>null</code>.
* @return The terminal listener instance.
*/
protected ITerminalListener doCreateTerminalTabTerminalListener(CTabItem item) {
Assert.isNotNull(item);
return new TabTerminalListener(item);
}
/**
* Creates a new terminal console tab dispose listener instance.
*
* @param parent The parent terminal console tab folder manager. Must be not <code>null</code>.
* @return The dispose listener instance.
*/
protected DisposeListener doCreateTerminalTabDisposeListener(TabFolderManager parent) {
Assert.isNotNull(parent);
return new TabDisposeListener(parent);
}
/**
* Creates a new terminal console tab property change listener instance.
*
* @param item The tab item. Must be not <code>null</code>.
* @return The property change listener instance.
*/
protected IPropertyChangeListener doCreateTerminalTabPropertyChangeListener(CTabItem item) {
Assert.isNotNull(item);
return new TabPropertyChangeListener(item);
}
/**
* Returns the tab item image.
*
* @param connector The terminal connector. Must be not <code>null</code>.
* @param data The custom terminal data node or <code>null</code>.
*
* @return The tab item image or <code>null</code>.
*/
protected Image getTabItemImage(ITerminalConnector connector, Object data) {
Assert.isNotNull(connector);
return UIPlugin.getImage(ImageConsts.VIEW_Terminals);
}
/**
* Lookup a tab item with the given title and the given terminal connector.
* <p>
* <b>Note:</b> The method will handle unified tab item titles itself.
*
* @param title The tab item title. Must be not <code>null</code>.
* @param connector The terminal connector. Must be not <code>null</code>.
* @param data The custom terminal data node or <code>null</code>.
*
* @return The corresponding tab item or <code>null</code>.
*/
public CTabItem findTabItem(String title, ITerminalConnector connector, Object data) {
Assert.isNotNull(title);
Assert.isNotNull(connector);
// Get the tab folder
CTabFolder tabFolder = getTabFolder();
if (tabFolder == null) return null;
// Loop all existing tab items and try to find a matching title
for (CTabItem item : tabFolder.getItems()) {
// Disposed items cannot be matched
if (item.isDisposed()) continue;
// Get the title from the current tab item
String itemTitle = item.getText();
// The terminal console state might be signaled to the user via the
// terminal console tab title. Filter out any prefix "<.*>\s*".
itemTitle = itemTitle.replaceFirst("^<.*>\\s*", ""); //$NON-NLS-1$ //$NON-NLS-2$
if (itemTitle.startsWith(title)) {
// The title string matches -> double check with the terminal connector
ITerminalViewControl terminal = (ITerminalViewControl)item.getData();
ITerminalConnector connector2 = terminal.getTerminalConnector();
// If the connector id and name matches -> check on the settings
if (connector.getId().equals(connector2.getId()) && connector.getName().equals(connector2.getName())) {
if (!connector.isInitialized()) {
// an uninitialized connector does not yield a sensible summary
return item;
}
String summary = connector.getSettingsSummary();
String summary2 = connector2.getSettingsSummary();
// If we have matching settings -> we've found the matching item
if (summary.equals(summary2)) return item;
}
}
}
return null;
}
/**
* Make the given tab item the active tab and bring the tab to the top.
*
* @param item The tab item. Must be not <code>null</code>.
*/
public void bringToTop(CTabItem item) {
Assert.isNotNull(item);
// Get the tab folder
CTabFolder tabFolder = getTabFolder();
if (tabFolder == null) return;
// Set the given tab item as selection to the tab folder
tabFolder.setSelection(item);
// Fire selection changed event
fireSelectionChanged();
}
/**
* Returns the currently active tab.
*
* @return The active tab item or <code>null</code> if none.
*/
public CTabItem getActiveTabItem() {
// Get the tab folder
CTabFolder tabFolder = getTabFolder();
if (tabFolder == null) return null;
return tabFolder.getSelection();
}
/**
* Remove all terminated tab items.
*/
public void removeTerminatedItems() {
// Get the tab folder
CTabFolder tabFolder = getTabFolder();
if (tabFolder == null) return;
// Loop the items and check for terminated status
for (CTabItem item: tabFolder.getItems()) {
// Disposed items cannot be matched
if (item.isDisposed()) continue;
// Check if the item is terminated
if (isTerminatedTabItem(item)) {
// item is terminated -> dispose
item.dispose();
}
}
}
/**
* Checks if the given tab item represents a terminated console. Subclasses may
* overwrite this method to extend the definition of terminated.
*
* @param item The tab item or <code>null</code>.
* @return <code>True</code> if the tab item represents a terminated console, <code>false</code> otherwise.
*/
protected boolean isTerminatedTabItem(CTabItem item) {
// Null items or disposed items cannot be matched
if (item == null || item.isDisposed()) return false;
// First, match the item title. If it contains "<terminated>", the item can be removed
String itemTitle = item.getText();
if (itemTitle != null && itemTitle.contains("<terminated>")) { //$NON-NLS-1$
return true;
}
// Second, check if the associated terminal control is closed
// The title string matches -> double check with the terminal connector
ITerminalViewControl terminal = (ITerminalViewControl)item.getData();
if (terminal != null && terminal.getState() == TerminalState.CLOSED) {
return true;
}
return false;
}
/* (non-Javadoc)
* @see org.eclipse.jface.viewers.ISelectionProvider#addSelectionChangedListener(org.eclipse.jface.viewers.ISelectionChangedListener)
*/
@Override
public void addSelectionChangedListener(ISelectionChangedListener listener) {
if (listener != null && !selectionChangedListeners.contains(listener)) selectionChangedListeners.add(listener);
}
/* (non-Javadoc)
* @see org.eclipse.jface.viewers.ISelectionProvider#removeSelectionChangedListener(org.eclipse.jface.viewers.ISelectionChangedListener)
*/
@Override
public void removeSelectionChangedListener(ISelectionChangedListener listener) {
if (listener != null) selectionChangedListeners.remove(listener);
}
/* (non-Javadoc)
* @see org.eclipse.jface.viewers.ISelectionProvider#getSelection()
*/
@Override
public ISelection getSelection() {
CTabItem activeTabItem = getActiveTabItem();
return activeTabItem != null ? new StructuredSelection(activeTabItem) : new StructuredSelection();
}
/* (non-Javadoc)
* @see org.eclipse.jface.viewers.ISelectionProvider#setSelection(org.eclipse.jface.viewers.ISelection)
*/
@Override
public void setSelection(ISelection selection) {
if (selection instanceof IStructuredSelection && !selection.isEmpty()) {
// The first selection element which is a CTabItem will become the active item
Iterator<?> iterator = ((IStructuredSelection)selection).iterator();
while (iterator.hasNext()) {
Object candidate = iterator.next();
if (candidate instanceof CTabItem) { bringToTop((CTabItem)candidate); return; }
}
}
// fire a changed event in any case
fireSelectionChanged(selection);
}
/**
* Fire the selection changed event to the registered listeners.
*/
protected void fireSelectionChanged() {
fireSelectionChanged(getSelection());
}
/**
* Fire the selection changed event to the registered listeners.
*/
protected final void fireSelectionChanged(ISelection selection) {
// Create the selection changed event
SelectionChangedEvent event = new SelectionChangedEvent(TabFolderManager.this, selection);
// First, invoke the registered listeners and let them do their job
for (ISelectionChangedListener listener : selectionChangedListeners) {
listener.selectionChanged(event);
}
// Second, broadcast the event if desired
if (isBroadcastSelectionChangedEvent()) onBroadcastSelectionChangedEvent(event);
}
/**
* Controls if or if not a selection changed event, processed by this tab
* folder manager shall be broadcasted to via the global Workbench notification
* mechanism.
*
* @return <code>True</code> to broadcast the selection changed event, <code>false</code> otherwise.
*/
protected boolean isBroadcastSelectionChangedEvent() {
return false;
}
/**
* Broadcasts the given selection changed event via the global Workbench notification mechanism.
*
* @param selectionChangedEvent The selection changed event or <code>null</code>.
*/
protected void onBroadcastSelectionChangedEvent(SelectionChangedEvent selectionChangedEvent) {
SelectionChangedBroadcastEvent event = doCreateSelectionChangedBroadcastEvent(this, selectionChangedEvent);
if (event != null) EventManager.getInstance().fireEvent(event);
}
/**
* Creates the selection changed broadcast event.
*
* @param source The event source. Must not be <code>null</code>.
* @param selectionChangedEvent The selection changed event or <code>null</code>.
*
* @return The selection changed broadcast event or <code>null</code>.
*/
protected SelectionChangedBroadcastEvent doCreateSelectionChangedBroadcastEvent(TabFolderManager source, SelectionChangedEvent selectionChangedEvent) {
return new SelectionChangedBroadcastEvent(source, selectionChangedEvent);
}
/**
* Returns if or if not this tab folder manager is listening to broadcasted selection
* changed events. Broadcasted events by the same tab folder manager are ignored independent
* of the methods return value.
*
* @return <code>True</code> to listen to broadcasted selection changed events, <code>false</code> to not listen.
*/
protected boolean isListeningToBroadcastedSelectionChangedEvent() {
return false;
}
/**
* Creates a new broadcasted selection changed event listener instance.
*
* @param parent The parent tab folder manager. Must not be <code>null</code>.
* @return The event listener instance or <code>null</code>.
*/
protected BroadcastedSelectionChangedEventListener doCreateBroadcastedSelectionChangedEventListener(TabFolderManager parent) {
return new BroadcastedSelectionChangedEventListener(parent);
}
}