/******************************************************************************* * Copyright (c) 2003, 2015 IBM Corporation 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: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.jface.action; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import org.eclipse.core.runtime.Assert; import org.eclipse.jface.internal.provisional.action.IToolBarManager2; import org.eclipse.jface.util.Policy; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.CoolBar; import org.eclipse.swt.widgets.CoolItem; import org.eclipse.swt.widgets.Menu; /** * A cool bar manager is a contribution manager which realizes itself and its * items in a cool bar control. * <p> * This class may be instantiated; it may also be subclassed. * </p> * * @since 3.0 */ public class CoolBarManager extends ContributionManager implements ICoolBarManager { /** * A separator created by the end user. */ public final static String USER_SEPARATOR = "UserSeparator"; //$NON-NLS-1$ /** * The original creation order of the contribution items. */ private ArrayList<IContributionItem> cbItemsCreationOrder = new ArrayList<>(); /** * MenuManager for cool bar pop-up menu, or null if none. */ private MenuManager contextMenuManager = null; /** * The cool bar control; <code>null</code> before creation and after * disposal. */ private CoolBar coolBar = null; /** * The cool bar items style; <code>SWT.NONE</code> by default. */ private int itemStyle = SWT.NONE; /** * Creates a new cool bar manager with the default style. Equivalent to * <code>CoolBarManager(SWT.NONE)</code>. */ public CoolBarManager() { // do nothing } /** * Creates a cool bar manager for an existing cool bar control. This * manager becomes responsible for the control, and will dispose of it when * the manager is disposed. * * @param coolBar * the cool bar control */ public CoolBarManager(CoolBar coolBar) { this(); Assert.isNotNull(coolBar); this.coolBar = coolBar; itemStyle = coolBar.getStyle(); } /** * Creates a cool bar manager with the given SWT style. Calling <code>createControl</code> * will create the cool bar control. * * @param style * the cool bar item style; see * {@link org.eclipse.swt.widgets.CoolBar CoolBar}for for valid * style bits */ public CoolBarManager(int style) { itemStyle = style; } @Override public void add(IToolBarManager toolBarManager) { Assert.isNotNull(toolBarManager); super.add(new ToolBarContributionItem(toolBarManager)); } /** * Collapses consecutive separators and removes a separator from the * beginning and end of the list. * * @param contributionList * the list of contributions; must not be <code>null</code>. * @return The contribution list provided with extraneous separators * removed; this value is never <code>null</code>, but may be * empty. */ private ArrayList<IContributionItem> adjustContributionList(ArrayList<IContributionItem> contributionList) { IContributionItem item; // Fist remove a separator if it is the first element of the list if (contributionList.size() != 0) { item = contributionList.get(0); if (item.isSeparator()) { contributionList.remove(0); } ListIterator<IContributionItem> iterator = contributionList.listIterator(); // collapse consecutive separators while (iterator.hasNext()) { item = iterator.next(); if (item.isSeparator()) { while (iterator.hasNext()) { item = iterator.next(); if (item.isSeparator()) { iterator.remove(); } else { break; } } } } if (contributionList.size() != 0) { // Now check last element to see if there is a separator item = contributionList.get(contributionList .size() - 1); if (item.isSeparator()) { contributionList.remove(contributionList.size() - 1); } } } return contributionList; } @Override protected boolean allowItem(IContributionItem itemToAdd) { /* We will allow as many null entries as they like, though there should * be none. */ if (itemToAdd == null) { return true; } /* Null identifiers can be expected in generic contribution items. */ String firstId = itemToAdd.getId(); if (firstId == null) { return true; } // Cycle through the current list looking for duplicates. IContributionItem[] currentItems = getItems(); for (IContributionItem currentItem : currentItems) { // We ignore null entries. if (currentItem == null) { continue; } String secondId = currentItem.getId(); if (firstId.equals(secondId)) { if (Policy.TRACE_TOOLBAR) { System.out.println("Trying to add a duplicate item."); //$NON-NLS-1$ new Exception().printStackTrace(System.out); System.out.println("DONE --------------------------"); //$NON-NLS-1$ } return false; } } return true; } /** * Positions the list iterator to the end of all the separators. Calling * <code>next()</code> the iterator should return the immediate object * following the last separator. * * @param iterator * the list iterator. */ private void collapseSeparators(ListIterator<IContributionItem> iterator) { while (iterator.hasNext()) { IContributionItem item = iterator.next(); if (!item.isSeparator()) { iterator.previous(); return; } } } /** * Returns whether the cool bar control has been created and not yet * disposed. * * @return <code>true</code> if the control has been created and not yet * disposed, <code>false</code> otherwise */ private boolean coolBarExist() { return coolBar != null && !coolBar.isDisposed(); } /** * Creates and returns this manager's cool bar control. Does not create a * new control if one already exists. * * @param parent * the parent control * @return the cool bar control */ public CoolBar createControl(Composite parent) { Assert.isNotNull(parent); if (!coolBarExist()) { coolBar = new CoolBar(parent, itemStyle); coolBar.setMenu(getContextMenuControl()); coolBar.setLocked(false); update(false); } return coolBar; } /** * Disposes of this cool bar manager and frees all allocated SWT resources. * Notifies all contribution items of the dispose. Note that this method * does not clean up references between this cool bar manager and its * associated contribution items. Use <code>removeAll</code> for that * purpose. */ public void dispose() { if (coolBarExist()) { coolBar.dispose(); coolBar = null; } IContributionItem[] items = getItems(); for (IContributionItem item : items) { // Disposes of the contribution item. // If Contribution Item is a toolbar then it will dispose of // all the nested // contribution items. item.dispose(); } // If a context menu existed then dispose of it. if (contextMenuManager != null) { contextMenuManager.dispose(); contextMenuManager = null; } } /** * Disposes the given cool item. * * @param item * the cool item to dispose */ private void dispose(CoolItem item) { if ((item != null) && !item.isDisposed()) { item.setData(null); Control control = item.getControl(); // if the control is already disposed, setting the coolitem // control to null will cause an SWT exception, workaround // for 19630 if ((control != null) && !control.isDisposed()) { item.setControl(null); // we created it, we dispose it, see bug 293433 control.dispose(); } item.dispose(); } } /** * Finds the cool item associated with the given contribution item. * * @param item * the contribution item * @return the associated cool item, or <code>null</code> if not found */ private CoolItem findCoolItem(IContributionItem item) { CoolItem[] coolItems = (coolBar == null) ? null : coolBar.getItems(); return findCoolItem(coolItems, item); } private CoolItem findCoolItem(CoolItem[] items, IContributionItem item) { if (items == null) { return null; } for (CoolItem coolItem : items) { IContributionItem data = (IContributionItem) coolItem.getData(); if (data != null && data.equals(item)) { return coolItem; } } return null; } /** * Return a consistent set of wrap indices. The return value will always * include at least one entry and the first entry will always be zero. * CoolBar.getWrapIndices() is inconsistent in whether or not it returns an * index for the first row. * * @param wraps * the wrap indicies from the cool bar widget * @return the adjusted wrap indicies. */ private int[] getAdjustedWrapIndices(int[] wraps) { int[] adjustedWrapIndices; if (wraps.length == 0) { adjustedWrapIndices = new int[] { 0 }; } else { if (wraps[0] != 0) { adjustedWrapIndices = new int[wraps.length + 1]; adjustedWrapIndices[0] = 0; for (int i = 0; i < wraps.length; i++) { adjustedWrapIndices[i + 1] = wraps[i]; } } else { adjustedWrapIndices = wraps; } } return adjustedWrapIndices; } /** * Returns the control of the Menu Manager. If the menu manager does not * have a control then one is created. * * @return menu control associated with manager, or null if none */ private Menu getContextMenuControl() { if ((contextMenuManager != null) && (coolBar != null)) { Menu menuWidget = contextMenuManager.getMenu(); if ((menuWidget == null) || (menuWidget.isDisposed())) { menuWidget = contextMenuManager.createContextMenu(coolBar); } return menuWidget; } return null; } @Override public IMenuManager getContextMenuManager() { return contextMenuManager; } /** * Returns the cool bar control for this manager. * * @return the cool bar control, or <code>null</code> if none */ public CoolBar getControl() { return coolBar; } /** * Returns an array list of all the contribution items in the manager. * * @return an array list of contribution items. */ private ArrayList<IContributionItem> getItemList() { IContributionItem[] cbItems = getItems(); ArrayList<IContributionItem> list = new ArrayList<>(cbItems.length); for (IContributionItem cbItem : cbItems) { list.add(cbItem); } return list; } @Override public boolean getLockLayout() { if (!coolBarExist()) { return false; } return coolBar.getLocked(); } /** * Returns the number of rows that should be displayed visually. * * @param items * the array of contributin items * @return the number of rows */ private int getNumRows(IContributionItem[] items) { int numRows = 1; boolean separatorFound = false; for (int i = 0; i < items.length; i++) { if (items[i].isSeparator()) { separatorFound = true; } if ((separatorFound) && (isChildVisible(items[i])) && (!items[i].isGroupMarker()) && (!items[i].isSeparator())) { numRows++; separatorFound = false; } } return numRows; } @Override public int getStyle() { return itemStyle; } /** * Subclasses may extend this <code>ContributionManager</code> method, * but must call <code>super.itemAdded</code>. * * @see org.eclipse.jface.action.ContributionManager#itemAdded(org.eclipse.jface.action.IContributionItem) */ @Override protected void itemAdded(IContributionItem item) { Assert.isNotNull(item); super.itemAdded(item); int insertedAt = indexOf(item); boolean replaced = false; final int size = cbItemsCreationOrder.size(); for (int i = 0; i < size; i++) { IContributionItem created = cbItemsCreationOrder .get(i); if (created.getId() != null && created.getId().equals(item.getId())) { cbItemsCreationOrder.set(i, item); replaced = true; break; } } if (!replaced) { cbItemsCreationOrder.add(Math.min(Math.max(insertedAt, 0), cbItemsCreationOrder.size()), item); } } /** * Subclasses may extend this <code>ContributionManager</code> method, * but must call <code>super.itemRemoved</code>. * * @see org.eclipse.jface.action.ContributionManager#itemRemoved(org.eclipse.jface.action.IContributionItem) */ @Override protected void itemRemoved(IContributionItem item) { Assert.isNotNull(item); super.itemRemoved(item); CoolItem coolItem = findCoolItem(item); if (coolItem != null) { coolItem.setData(null); } } /** * Positions the list iterator to the starting of the next row. By calling * next on the returned iterator, it will return the first element of the * next row. * * @param iterator * the list iterator of contribution items * @param ignoreCurrentItem * Whether the current item in the iterator should be considered * (as well as subsequent items). */ private void nextRow(ListIterator<IContributionItem> iterator, boolean ignoreCurrentItem) { IContributionItem currentElement = null; if (!ignoreCurrentItem && iterator.hasPrevious()) { currentElement = iterator.previous(); iterator.next(); } if ((currentElement != null) && (currentElement.isSeparator())) { collapseSeparators(iterator); return; } //Find next separator while (iterator.hasNext()) { IContributionItem item = iterator.next(); if (item.isSeparator()) { // we we find a separator, collapse any consecutive // separators // and return collapseSeparators(iterator); return; } } } /* * Used for debuging. Prints all the items in the internal structures. */ // private void printContributions(ArrayList contributionList) { // int index = 0; // System.out.println("----------------------------------\n"); //$NON-NLS-1$ // for (Iterator i = contributionList.iterator(); i.hasNext(); index++) { // IContributionItem item = (IContributionItem) i.next(); // if (item.isSeparator()) { // System.out.println("Separator"); //$NON-NLS-1$ // } else { // System.out.println(index + ". Item id: " + item.getId() //$NON-NLS-1$ // + " - is Visible: " //$NON-NLS-1$ // + item.isVisible()); // } // } // } /** * Synchronizes the visual order of the cool items in the control with this * manager's internal data structures. This method should be called before * requesting the order of the contribution items to ensure that the order * is accurate. * <p> * Note that <code>update()</code> and <code>refresh()</code> are * converses: <code>update()</code> changes the visual order to match the * internal structures, and <code>refresh</code> changes the internal * structures to match the visual order. * </p> */ public void refresh() { if (!coolBarExist()) { return; } // Retreives the list of contribution items as an array list ArrayList<IContributionItem> contributionList = getItemList(); // Check the size of the list if (contributionList.size() == 0) { return; } // The list of all the cool items in their visual order CoolItem[] coolItems = coolBar.getItems(); // The wrap indicies of the coolbar int[] wrapIndicies = getAdjustedWrapIndices(coolBar.getWrapIndices()); int row = 0; int coolItemIndex = 0; // Traverse through all cool items in the coolbar add them to a new // data structure // in the correct order ArrayList<IContributionItem> displayedItems = new ArrayList<>(coolBar.getItemCount()); for (int i = 0; i < coolItems.length; i++) { CoolItem coolItem = coolItems[i]; if (coolItem.getData() instanceof IContributionItem) { IContributionItem cbItem = (IContributionItem) coolItem .getData(); displayedItems.add(Math.min(i, displayedItems.size()), cbItem); } } // Add separators to the displayed Items data structure int offset = 0; for (int i = 1; i < wrapIndicies.length; i++) { int insertAt = wrapIndicies[i] + offset; displayedItems.add(insertAt, new Separator(USER_SEPARATOR)); offset++; } // Determine which rows are invisible ArrayList<Integer> existingVisibleRows = new ArrayList<>(4); ListIterator<IContributionItem> rowIterator = contributionList.listIterator(); collapseSeparators(rowIterator); int numRow = 0; while (rowIterator.hasNext()) { // Scan row while (rowIterator.hasNext()) { IContributionItem cbItem = rowIterator .next(); if (displayedItems.contains(cbItem)) { existingVisibleRows.add(Integer.valueOf(numRow)); break; } if (cbItem.isSeparator()) { break; } } nextRow(rowIterator, false); numRow++; } Iterator<Integer> existingRows = existingVisibleRows.iterator(); // Adjust row number to the first visible if (existingRows.hasNext()) { row = existingRows.next().intValue(); } HashMap<IContributionItem, Integer> itemLocation = new HashMap<>(); for (ListIterator<IContributionItem> locationIterator = displayedItems.listIterator(); locationIterator .hasNext();) { IContributionItem item = locationIterator .next(); if (item.isSeparator()) { if (existingRows.hasNext()) { Integer value = existingRows.next(); row = value.intValue(); } else { row++; } } else { itemLocation.put(item, Integer.valueOf(row)); } } // Insert the contribution items in their correct location for (ListIterator<IContributionItem> iterator = displayedItems.listIterator(); iterator .hasNext();) { IContributionItem cbItem = iterator.next(); if (cbItem.isSeparator()) { coolItemIndex = 0; } else { relocate(cbItem, coolItemIndex, contributionList, itemLocation); cbItem.saveWidgetState(); coolItemIndex++; } } contributionList = adjustContributionList(contributionList); if (contributionList.size() != 0) { IContributionItem[] array = new IContributionItem[contributionList .size() - 1]; array = contributionList.toArray(array); internalSetItems(array); } } /** * Relocates the given contribution item to the specified index. * * @param cbItem * the conribution item to relocate * @param index * the index to locate this item * @param contributionList * the current list of conrtributions * @param itemLocation */ private void relocate(IContributionItem cbItem, int index, ArrayList<IContributionItem> contributionList, HashMap<IContributionItem, Integer> itemLocation) { if ((itemLocation.get(cbItem) == null)) { return; } int targetRow = itemLocation.get(cbItem).intValue(); int cbInternalIndex = contributionList.indexOf(cbItem); // by default add to end of list int insertAt = contributionList.size(); // Find the row to place this item in. ListIterator<IContributionItem> iterator = contributionList.listIterator(); // bypass any separators at the begining collapseSeparators(iterator); int currentRow = -1; while (iterator.hasNext()) { currentRow++; if (currentRow == targetRow) { // We found the row to insert the item int virtualIndex = 0; insertAt = iterator.nextIndex(); // first check the position of the current element (item) // then get the next element while (iterator.hasNext()) { IContributionItem item = iterator .next(); Integer itemRow = itemLocation.get(item); if (item.isSeparator()) { break; } // if the item has an associate widget if ((itemRow != null) && (itemRow.intValue() == targetRow)) { // if the next element is the index we are looking for // then break if (virtualIndex >= index) { break; } virtualIndex++; } insertAt++; } // If we don't need to move it then we return if (cbInternalIndex == insertAt) { return; } break; } nextRow(iterator, true); } contributionList.remove(cbItem); // Adjust insertAt index if (cbInternalIndex < insertAt) { insertAt--; } // if we didn't find the row then add a new row if (currentRow != targetRow) { contributionList.add(new Separator(USER_SEPARATOR)); insertAt = contributionList.size(); } insertAt = Math.min(insertAt, contributionList.size()); contributionList.add(insertAt, cbItem); } /** * Restores the canonical order of this cool bar manager. The canonical * order is the order in which the contribution items where added. */ public void resetItemOrder() { for (ListIterator<IContributionItem> iterator = cbItemsCreationOrder.listIterator(); iterator .hasNext();) { IContributionItem item = iterator.next(); // if its a user separator then do not include in original order. if ((item.getId() != null) && (item.getId().equals(USER_SEPARATOR))) { iterator.remove(); } } IContributionItem[] itemsToSet = new IContributionItem[cbItemsCreationOrder .size()]; cbItemsCreationOrder.toArray(itemsToSet); setItems(itemsToSet); } @Override public void setContextMenuManager(IMenuManager contextMenuManager) { this.contextMenuManager = (MenuManager) contextMenuManager; if (coolBar != null) { coolBar.setMenu(getContextMenuControl()); } } /** * Replaces the current items with the given items. * Forces an update. * * @param newItems the items with which to replace the current items */ public void setItems(IContributionItem[] newItems) { // dispose of all the cool items on the cool bar manager if (coolBar != null) { CoolItem[] coolItems = coolBar.getItems(); for (CoolItem coolItem : coolItems) { dispose(coolItem); } } // Set the internal structure to this order internalSetItems(newItems); // Force and update update(true); } @Override public void setLockLayout(boolean value) { if (!coolBarExist()) { return; } coolBar.setLocked(value); } /** * Subclasses may extend this <code>IContributionManager</code> method, * but must call <code>super.update</code>. * * @see org.eclipse.jface.action.IContributionManager#update(boolean) */ @Override public void update(boolean force) { if ((!isDirty() && !force) || (!coolBarExist())) { return; } boolean relock = false; boolean changed = false; try { coolBar.setRedraw(false); // Refresh the widget data with the internal data structure. refresh(); if (coolBar.getLocked()) { coolBar.setLocked(false); relock = true; } /* * Make a list of items including only those items that are * visible. Separators should stay because they mark line breaks in * a cool bar. */ final IContributionItem[] items = getItems(); final List<IContributionItem> visibleItems = new ArrayList<>(items.length); for (final IContributionItem item : items) { if (isChildVisible(item)) { visibleItems.add(item); } } /* * Make a list of CoolItem widgets in the cool bar for which there * is no current visible contribution item. These are the widgets * to be disposed. Dynamic items are also removed. */ CoolItem[] coolItems = coolBar.getItems(); final ArrayList<CoolItem> coolItemsToRemove = new ArrayList<>(coolItems.length); for (CoolItem coolItem : coolItems) { final Object data = coolItem.getData(); if ((data == null) || (!visibleItems.contains(data)) || ((data instanceof IContributionItem) && ((IContributionItem) data) .isDynamic())) { coolItemsToRemove.add(coolItem); } } // Dispose of any items in the list to be removed. for (int i = coolItemsToRemove.size() - 1; i >= 0; i--) { CoolItem coolItem = coolItemsToRemove.get(i); if (!coolItem.isDisposed()) { Control control = coolItem.getControl(); if (control != null) { coolItem.setControl(null); control.dispose(); } coolItem.dispose(); } } // Add any new items by telling them to fill. coolItems = coolBar.getItems(); IContributionItem sourceItem; IContributionItem destinationItem; int sourceIndex = 0; int destinationIndex = 0; final Iterator<IContributionItem> visibleItemItr = visibleItems.iterator(); while (visibleItemItr.hasNext()) { sourceItem = visibleItemItr.next(); // Retrieve the corresponding contribution item from SWT's // data. if (sourceIndex < coolItems.length) { destinationItem = (IContributionItem) coolItems[sourceIndex] .getData(); } else { destinationItem = null; } // The items match is they are equal or both separators. if (destinationItem != null) { if (sourceItem.equals(destinationItem)) { sourceIndex++; destinationIndex++; sourceItem.update(); continue; } else if ((destinationItem.isSeparator()) && (sourceItem.isSeparator())) { coolItems[sourceIndex].setData(sourceItem); sourceIndex++; destinationIndex++; sourceItem.update(); continue; } } // Otherwise, a new item has to be added. final int start = coolBar.getItemCount(); if (sourceItem instanceof ToolBarContributionItem) { IToolBarManager manager = ((ToolBarContributionItem)sourceItem).getToolBarManager(); if(manager instanceof IToolBarManager2) { ((IToolBarManager2)manager).setOverrides(getOverrides()); } } sourceItem.fill(coolBar, destinationIndex); final int newItems = coolBar.getItemCount() - start; for (int i = 0; i < newItems; i++) { coolBar.getItem(destinationIndex++).setData(sourceItem); } changed = true; } // Remove any old widgets not accounted for. for (int i = coolItems.length - 1; i >= sourceIndex; i--) { final CoolItem item = coolItems[i]; if (!item.isDisposed()) { Control control = item.getControl(); if (control != null) { item.setControl(null); control.dispose(); } item.dispose(); changed = true; } } // Update wrap indices. updateWrapIndices(); // Update the sizes. for (IContributionItem item : items) { item.update(SIZE); } // if the coolBar was previously locked then lock it if (relock) { coolBar.setLocked(true); } if (changed) { updateTabOrder(); } // We are no longer dirty. setDirty(false); } finally { coolBar.setRedraw(true); } } /** * Sets the tab order of the coolbar to the visual order of its items. */ /* package */void updateTabOrder() { if (coolBar != null) { CoolItem[] items = coolBar.getItems(); if (items != null) { ArrayList<Control> children = new ArrayList<>(items.length); for (int i = 0; i < items.length; i++) { if ((items[i].getControl() != null) && (!items[i].getControl().isDisposed())) { children.add(items[i].getControl()); } } // Convert array Control[] childrenArray = new Control[0]; childrenArray = children.toArray(childrenArray); if (childrenArray != null) { coolBar.setTabList(childrenArray); } } } } /** * Updates the indices at which the cool bar should wrap. */ private void updateWrapIndices() { final IContributionItem[] items = getItems(); final int numRows = getNumRows(items) - 1; // Generate the list of wrap indices. final int[] wrapIndices = new int[numRows]; boolean foundSeparator = false; int j = 0; CoolItem[] coolItems = (coolBar == null) ? null : coolBar.getItems(); for (IContributionItem item : items) { CoolItem coolItem = findCoolItem(coolItems, item); if (item.isSeparator()) { foundSeparator = true; } if ((!item.isSeparator()) && (!item.isGroupMarker()) && (isChildVisible(item)) && (coolItem != null) && (foundSeparator)) { wrapIndices[j] = coolBar.indexOf(coolItem); j++; foundSeparator = false; } } /* * Check to see if these new wrap indices are different than the old * ones. */ final int[] oldIndices = coolBar.getWrapIndices(); boolean shouldUpdate = false; if (oldIndices.length == wrapIndices.length) { for (int i = 0; i < oldIndices.length; i++) { if (oldIndices[i] != wrapIndices[i]) { shouldUpdate = true; break; } } } else { shouldUpdate = true; } if (shouldUpdate) { coolBar.setWrapIndices(wrapIndices); } } private boolean isChildVisible(IContributionItem item) { Boolean v; IContributionManagerOverrides overrides = getOverrides(); if(overrides == null) { v = null; } else { v = getOverrides().getVisible(item); } if (v != null) { return v.booleanValue(); } return item.isVisible(); } }