connectivity/com.nokia.carbide.remoteConnections/src/com/nokia/carbide/remoteconnections/internal/ui/ConnectionStatusSelectorContribution.java
author Ed Swartz <ed.swartz@nokia.com>
Tue, 29 Dec 2009 11:18:15 -0600
changeset 723 81b14a47b8dc
parent 721 dbd80b3d41cc
child 724 6ab9c30e708f
permissions -rw-r--r--
Update UI for Remote Connections status widget and News Reader to behave the same (bug #10411)

/*
* Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies).
* All rights reserved.
* This component and the accompanying materials are made available
* under the terms of the License "Eclipse Public License v1.0"
* which accompanies this distribution, and is available
* at the URL "http://www.eclipse.org/legal/epl-v10.html".
*
* Initial Contributors:
* Nokia Corporation - initial contribution.
*
* Contributors:
*
* Description: 
*
*/

package com.nokia.carbide.remoteconnections.internal.ui;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CLabel;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
import org.eclipse.swt.widgets.ToolTip;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.menus.CommandContributionItem;
import org.eclipse.ui.menus.WorkbenchWindowControlContribution;

import com.nokia.carbide.remoteconnections.Messages;
import com.nokia.carbide.remoteconnections.RemoteConnectionsActivator;
import com.nokia.carbide.remoteconnections.interfaces.IConnectedService;
import com.nokia.carbide.remoteconnections.interfaces.IConnection;
import com.nokia.carbide.remoteconnections.interfaces.IConnectionsManager;
import com.nokia.carbide.remoteconnections.interfaces.IConnectedService.IStatus;
import com.nokia.carbide.remoteconnections.interfaces.IConnectedService.IStatusChangedListener;
import com.nokia.carbide.remoteconnections.interfaces.IConnectionsManager.IConnectionListener;
import com.nokia.carbide.remoteconnections.interfaces.IConnectionsManager.IConnectionsManagerListener;
import com.nokia.carbide.remoteconnections.internal.api.IConnection2;
import com.nokia.carbide.remoteconnections.internal.api.IConnection2.IConnectionStatus;
import com.nokia.carbide.remoteconnections.internal.api.IConnection2.IConnectionStatusChangedListener;
import com.nokia.carbide.remoteconnections.internal.registry.Registry;
import com.nokia.carbide.remoteconnections.view.ConnectionsView;
import com.nokia.cpp.internal.api.utils.core.TextUtils;
import com.nokia.cpp.internal.api.utils.ui.WorkbenchUtils;


/**
 * This widget appears in the Eclipse trim and allows the user to select the
 * "default" device connection and also see its status at a glance.
 * <p>
 * Note: the UI for this control should behave similarly to that of the News Reader
 * trim.  Due to the way we're modifying the icon and the state dynamically
 * for this contribution, they can't be implemented the same way, however.
 */
@SuppressWarnings("deprecation")
public class ConnectionStatusSelectorContribution extends WorkbenchWindowControlContribution {

	/**
	 * This is a portion of the ICommand id used for the icon in the
	 * widget.  Keep this in sync with the extension point!
	 */
	private static final String OPEN_REMOTE_CONNECTIONS_VIEW_COMMAND_SUFFIX = "openRemoteConnectionsView";
	private Composite container;
	private CLabel connectionInfo;
	private ToolItem connectionIcon;
	private IConnectionsManager manager;
	private IConnection defaultConnection;
	private ListenerBlock listenerBlock;
	private ToolTip tooltip;
	private MouseAdapter toolbarListener;

	/**
	 * Contains all the listeners.  In most cases we just recreate the contribution status item.
	 */
	class ListenerBlock implements IConnectionListener, IConnectionsManagerListener, IStatusChangedListener, IConnectionStatusChangedListener {

		/* (non-Javadoc)
		 * @see com.nokia.carbide.remoteconnections.interfaces.IConnectionsManager.IConnectionListener#connectionAdded(com.nokia.carbide.remoteconnections.interfaces.IConnection)
		 */
		public void connectionAdded(IConnection connection) {
			updateUI();
			boolean display = (connection instanceof IConnection2 && ((IConnection2) connection).isDynamic());
			if (display)
				launchBubble(MessageFormat.format(
						Messages.getString("ConnectionStatusSelectorContribution.AddedConnectionFormat"),  //$NON-NLS-1$
						connection.getDisplayName()));
		}
		
		/* (non-Javadoc)
		 * @see com.nokia.carbide.remoteconnections.interfaces.IConnectionsManager.IConnectionListener#connectionRemoved(com.nokia.carbide.remoteconnections.interfaces.IConnection)
		 */
		public void connectionRemoved(IConnection connection) {
			updateUI();
			boolean display = (connection instanceof IConnection2 && ((IConnection2) connection).isDynamic());
			if (display) 
				launchBubble(MessageFormat.format(
						Messages.getString("ConnectionStatusSelectorContribution.RemovedConnectionFormat"),  //$NON-NLS-1$
						connection.getDisplayName()));
		}
		
		/* (non-Javadoc)
		 * @see com.nokia.carbide.remoteconnections.interfaces.IConnectionsManager.IConnectionListener#defaultConnectionSet(com.nokia.carbide.remoteconnections.interfaces.IConnection)
		 */
		public void defaultConnectionSet(IConnection connection) {
			updateUI();
		}

		/* (non-Javadoc)
		 * @see com.nokia.carbide.remoteconnections.interfaces.IConnectionsManager.IConnectionsManagerListener#connectionStoreChanged()
		 */
		public void connectionStoreChanged() {
			updateUI();
		}

		/* (non-Javadoc)
		 * @see com.nokia.carbide.remoteconnections.interfaces.IConnectionsManager.IConnectionsManagerListener#displayChanged()
		 */
		public void displayChanged() {
			updateUI();
		}

		/* (non-Javadoc)
		 * @see com.nokia.carbide.remoteconnections.interfaces.IConnectedService.IStatusChangedListener#statusChanged(com.nokia.carbide.remoteconnections.interfaces.IConnectedService.IStatus)
		 */
		public void statusChanged(IStatus status) {
			updateConnectionStatus(null);
		}

		/* (non-Javadoc)
		 * @see com.nokia.carbide.remoteconnections.internal.api.IConnection2.IConnectionStatusChangedListener#statusChanged(com.nokia.carbide.remoteconnections.internal.api.IConnection2.IConnectionStatus)
		 */
		public void statusChanged(IConnectionStatus status) {
			updateConnectionStatus(status);
		}

	}
	
	public ConnectionStatusSelectorContribution() {
		manager = RemoteConnectionsActivator.getConnectionsManager();
		listenerBlock = new ListenerBlock();
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.eclipse.jface.action.ControlContribution#createControl(org.eclipse.swt.widgets.Composite)
	 */
	@Override
	protected Control createControl(Composite parent) {
		
		// This UI is recreated whenever the default connection changes.
		
		// This is gross.  The normal parent of this is a toolbar,
		// but we cannot add arbitrary stuff to the toolbar besides
		// this control.  (If you try, it will usually show up
		// out of order!)
		//
		// But we want to have a toolbar button for the connection icon
		// for which we can change the tooltip and image, and attach 
		// a mouse listener responds to left and right clicking.
		//
		// In order to do this, we need to poke around in the widget tree
		// to find the expected Eclipse-generated toolitem.  Unfortunately,
		// controlling all this ourselves is even uglier (I tried).

		final ToolBar toolbar; 
		if (parent instanceof ToolBar) {
			toolbar = (ToolBar) parent;
			ToolItem[] items = toolbar.getItems();
			for (ToolItem item : items) {
				Object data = item.getData();
				if (data instanceof CommandContributionItem &&
						((CommandContributionItem) data).getId().contains(OPEN_REMOTE_CONNECTIONS_VIEW_COMMAND_SUFFIX)) {
					connectionIcon = item;
					break;
				}
			}
		} else {
			toolbar = null;
		}
		
		container = new Composite(parent, SWT.NONE);
		GridLayoutFactory.fillDefaults().margins(2, 0).applyTo(container);

		// Create a label for the trim, outside the toolbar.
		connectionInfo = new CLabel(container, SWT.NONE);
		GridDataFactory.fillDefaults().grab(false, true).applyTo(connectionInfo);

		String text = Messages.getString("ConnectionStatusSelectorContribution_NoDefaultConnectionMessage"); //$NON-NLS-1$
		defaultConnection = manager.getDefaultConnection();
		if (defaultConnection != null)
			text = defaultConnection.getDisplayName();
		
		connectionInfo.setText(text);

		attachListeners();
		
		updateConnectionStatus(getConnectionStatus(defaultConnection));
		

		// Yuck, toolbars and items have a wonky UI.  We need to do these events on the parent,
		// since the ToolItem itself is just an area inside the parent.  (#getControl() is only for separators ?!)
			
		// On icon: left click = open view, right click = menu
		
		if (toolbar != null) {
			if (toolbarListener != null)
				toolbar.removeMouseListener(toolbarListener);
			
			toolbarListener = new MouseAdapter() {
				/* (non-Javadoc)
				 * @see org.eclipse.swt.events.MouseAdapter#mouseDown(org.eclipse.swt.events.MouseEvent)
				 */
				@Override
				public void mouseDown(MouseEvent event) {
					ToolItem item = toolbar.getItem(new Point(event.x, event.y));
					if (item == connectionIcon) {
						if (event.button == 1) {
							openConnectionsView();
						} else if (event.button == 3) {
							Point screenLoc = toolbar.toDisplay(event.x, event.y);
							handleConnectionMenu(screenLoc);
						}
					}
				}
			};
			toolbar.addMouseListener(toolbarListener);
			
			// On label: left or right click = menu
			connectionInfo.addMouseListener(new MouseAdapter() {
				public void mouseDown(MouseEvent event) {
					if (event.button == 1 || event.button == 3) {
						Point screenLoc = toolbar.toDisplay(event.x, event.y);
						handleConnectionMenu(screenLoc);
					}
				}
			});
		}
		
		RemoteConnectionsActivator.setHelp(container, "ConnectionStatusSelector"); //$NON-NLS-1$
		return container;
	}

	private void handleConnectionMenu(Point screenLoc) {
		Shell shell = connectionInfo.getParent().getShell();
		final Display display = shell.getDisplay();
		
		final Menu menu = new Menu(shell, SWT.POP_UP);
		populateConnectionMenu(menu);
		
		menu.setLocation(screenLoc.x, screenLoc.y);
		menu.setVisible(true);
		
		while (!menu.isDisposed() && menu.isVisible()) {
			if (!display.readAndDispatch())
				display.sleep();
		}
		menu.dispose();
	
	}

	/**
	 * @return
	 */
	protected void populateConnectionMenu(Menu menu) {
		// Display the connections with dynamic ones first, 
		// then static ones, separated by a separator
	
		List<IConnection> dynamicConnections = new ArrayList<IConnection>();
		List<IConnection> staticConnections = new ArrayList<IConnection>();
		for (IConnection connection : RemoteConnectionsActivator.getConnectionsManager().getConnections()) {
			if (connection instanceof IConnection2 && ((IConnection2)connection).isDynamic()) 
				dynamicConnections.add(connection);
			else
				staticConnections.add(connection);
		}
		
	
		Comparator<IConnection> connectionComparator = new Comparator<IConnection>() {
			public int compare(IConnection o1, IConnection o2) {
				return o1.getDisplayName().compareToIgnoreCase(o2.getDisplayName());
			}
		};
		Collections.sort(dynamicConnections, connectionComparator);
		Collections.sort(staticConnections, connectionComparator);
	
		MenuItem label = new MenuItem(menu, SWT.NONE);
		label.setEnabled(false);
		
		int number = 1;
		if (dynamicConnections.size() + staticConnections.size() == 0) {
			label.setText(Messages.getString("ConnectionStatusSelectorContribution.NoConnectionsDefinedOrDetected")); //$NON-NLS-1$
		} else {
			label.setText(Messages.getString("ConnectionStatusSelectorContribution_SelectTheDefaultConnectionMessage")); //$NON-NLS-1$
			
			for (IConnection connection : dynamicConnections) {
				createConnectionMenuItem(menu, connection, defaultConnection, number++);
			}
			
			new MenuItem(menu, SWT.SEPARATOR);
			
			for (IConnection connection : staticConnections) {
				createConnectionMenuItem(menu, connection, defaultConnection, number++);
			}
		}
		
		new MenuItem(menu, SWT.SEPARATOR);
		
		MenuItem openView = new MenuItem(menu, SWT.PUSH);
		openView.setText(Messages.getString("ConnectionStatusSelectorContribution.OpenRemoteConnectionsView")); //$NON-NLS-1$
		openView.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				openConnectionsView();
			}
		});
	}

	/**
	 * @param menu
	 * @param connection
	 * @param defaultConnection 
	 */
	private MenuItem createConnectionMenuItem(Menu menu, 
			final IConnection connection, 
			IConnection defaultConnection,
			int number) {
		MenuItem item = new MenuItem(menu, SWT.CHECK);
		
		boolean isDefault = false;
		isDefault = connection.equals(defaultConnection);
		
		item.setSelection(isDefault);
		
		item.setText("&" + number + " - " + connection.getDisplayName());
		
		item.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				manager.setDefaultConnection(connection);
			}
		});		
		
		return item;
	}

	private void attachListeners() {
		manager.addConnectionListener(listenerBlock);
		Registry.instance().addConnectionStoreChangedListener(listenerBlock);
		
		if (defaultConnection != null) {
			if (defaultConnection instanceof IConnection2) {
				((IConnection2) defaultConnection).addStatusChangedListener(listenerBlock);
			}
			for (IConnectedService service : manager.getConnectedServices(defaultConnection)) {
				service.addStatusChangedListener(listenerBlock);
			}
		}
	}

	private void removeListeners() {
		if (defaultConnection != null) {
			if (defaultConnection instanceof IConnection2) {
				((IConnection2) defaultConnection).removeStatusChangedListener(listenerBlock);
			}
			for (IConnectedService service : manager.getConnectedServices(defaultConnection)) {
				service.removeStatusChangedListener(listenerBlock);
			}
		}
		manager.removeConnectionListener(listenerBlock);
		Registry.instance().removeConnectionStoreChangedListener(listenerBlock);
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.jface.action.ContributionItem#dispose()
	 */
	public void dispose() {
		removeListeners();
		if (connectionIcon != null)
			connectionIcon.dispose();
		if (connectionInfo != null && !connectionInfo.isDisposed()) {
			if (toolbarListener != null && !connectionInfo.getParent().isDisposed())
				connectionInfo.getParent().removeMouseListener(toolbarListener);
			toolbarListener = null;
			connectionInfo.dispose();
		}
		super.dispose();
	}

	/**
	 * @param ev
	 */
	protected void openConnectionsView() {
		try {
			WorkbenchUtils.getView(ConnectionsView.VIEW_ID);
        } 
        catch (PartInitException e) {
        	RemoteConnectionsActivator.logError(e);
        }
	}

	/**
	 * @param defaultConnection
	 * @param status
	 * @return
	 */
	private String createConnectionStatusTooltip(IConnection defaultConnection,
			IConnectionStatus status) {
		if (defaultConnection == null) {
			return Messages.getString("ConnectionStatusSelectorContribution.NoDynamicOrManualConnectionsTooltip"); //$NON-NLS-1$
		}
		
		String statusString = null;
		if (status != null) {
			statusString = status.getDescription();
		}
		
		if (TextUtils.isEmpty(statusString)) {
			// fallback string; the status should not be empty
			if (ConnectionUIUtils.isSomeServiceInUse(defaultConnection))
				statusString = Messages.getString("ConnectionStatusSelectorContribution.InUse"); //$NON-NLS-1$
			else
				statusString = Messages.getString("ConnectionStatusSelectorContribution.NotInUse"); //$NON-NLS-1$
		}
		
		return MessageFormat.format(Messages.getString("ConnectionStatusSelectorContribution.ConnectionStatusFormat"), defaultConnection.getDisplayName(), statusString); //$NON-NLS-1$
	}

	/**
	 * Get the image representing the connection status.
	 * @param connection
	 * @return Image, to be disposed
	 */
	private IConnectionStatus getConnectionStatus(IConnection connection) {
		if (!(connection instanceof IConnection2)) {
			return null;
		} else {
			return ((IConnection2) connection).getStatus();
		}
	}
	
	/**
	 * @param status
	 */
	private void updateConnectionStatus(final IConnectionStatus status) {
		Display.getDefault().syncExec(new Runnable() {
			public void run() {
				if (connectionIcon == null || connectionIcon.isDisposed())
					return;
				
				Image statusImage;
				if (status != null)
					statusImage = ConnectionUIUtils.getConnectionStatusImage(status);
				else
					statusImage = ConnectionUIUtils.getConnectionImage(defaultConnection);
				
				connectionIcon.setImage(statusImage);
				String tip = createConnectionStatusTooltip(defaultConnection, status);
				connectionInfo.setToolTipText(tip);
				
				String preamble = "Click to open the Remote Connections View.\n\n";
				connectionIcon.setToolTipText(preamble + tip);		
			}
		});
		
	}

	/**
	 * 
	 */
	private void updateUI() {
		// perform update in UI thread
		final IWorkbench workbench = PlatformUI.getWorkbench();
		workbench.getDisplay().asyncExec(new Runnable() {
			public void run() {
				IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
				if (window != null) {
					update();
				}
			}
		});		
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.jface.action.ContributionItem#isDynamic()
	 */
	@Override
	public boolean isDynamic() {
		return true;
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.jface.action.ContributionItem#update()
	 */
	@Override
	public void update() {
		getParent().update(true);
	}
	
	/**
	 * Show a status bubble for important changes.
	 * @param string
	 */
	public void launchBubble(final String string) {
		Display.getDefault().asyncExec(new Runnable() {
			public void run() {
				if (tooltip != null)
					tooltip.dispose();
				
				if (connectionInfo == null || connectionInfo.isDisposed())
					return;
				
				tooltip = new ToolTip(connectionInfo.getParent().getShell(), SWT.BALLOON | SWT.ICON_INFORMATION);
				tooltip.setMessage(string);
				Rectangle bounds = connectionInfo.getBounds();
				Point center = new Point(bounds.x + bounds.width / 2, 
						bounds.y + bounds.height);
				Point location = connectionInfo.getParent().toDisplay(center);
				//System.out.println(connectionInfo.hashCode() + ": " + connectionInfo.getLocation() + " : " + location);
				tooltip.setLocation(location);
				tooltip.setVisible(true);
			}
		});
	}


}