/******************************************************************************* * Copyright (c) 2008, 2011 Thomas Holland (thomas@innot.de) 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: * Thomas Holland - initial API and implementation *******************************************************************************/ package de.innot.avreclipse.ui.controls; 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.TreeColumnLayout; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.viewers.ColumnPixelData; import org.eclipse.jface.viewers.ColumnWeightData; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeColumn; import org.eclipse.swt.widgets.TreeItem; import de.innot.avreclipse.core.toolinfo.fuses.BitFieldDescription; import de.innot.avreclipse.core.toolinfo.fuses.ByteValues; import de.innot.avreclipse.core.toolinfo.fuses.ConversionResults.ConversionStatus; import de.innot.avreclipse.core.util.AVRMCUidConverter; /** * Custom SWT Control to display the values of a {@link ByteValues} object. * <p> * This class uses a Tree to display the values of each byte and their associated bitfields. There * is also a Label at the top to indicate MCU and Fuse/Lockbit type. * </p> * <p> * It is extended from <code>Composite</code>, but should be used like a <code>Control</code>. For * example the {@link #setLayout(org.eclipse.swt.widgets.Layout)} method should not be used, because * this class already uses its own layout. * </p> * <dl> * <dt><b>Styles:</b></dt> * <dd>BORDER</dd> * <dt><b>Events:</b></dt> * <dd>(none)</dd> * </dl> * * @author Thomas Holland * @since 2.3 * */ public class FuseBytePreviewControl extends Composite { // The GUI Widgets private final Tree fTree; private final Label fHeaderLabel; /** * The MCU of the values currently displayed. Used to determine if the Tree can be updated or * has to be redrawn for a new ByteValue Object. */ private String fMCUid; private ByteValues fCurrentValues; /** List of the root TreeItems, the items representing complete Bytes. */ private final List<TreeItem> fRootItems = new ArrayList<TreeItem>(); /** The font used for the normal bitfield items */ private final Font fDialogFont = JFaceResources.getDialogFont(); /** The bold font used for the root items */ private final Font fBoldDialogFont = JFaceResources .getFontRegistry() .getBold( JFaceResources.DIALOG_FONT); // The three columns used by the tree private final static int COLUMN_NAME = 0; private final static int COLUMN_VALUE = 1; private final static int COLUMN_BITS = 2; // Flags to indicated if short or long column content is requested private boolean fShortName = false; private boolean fShortValue = false; // And the associated column header strings private final static String[] COLUMN_HEADER_NAMES = new String[] { "Name (short)", "Name (full)" }; private final static String[] COLUMN_HEADER_VALUES = new String[] { "Value (hex)", "Value (text)" }; // Tags used for the TreeItem.setData(String tag, value) method to pass // meta-information about the item to the PaintListener /** Property for the bitfield description (BitFieldDescription). */ private final static String TAG_BITFIELDDESCRIPTION = "bfd"; /** Property for the current value of the byte containing the bitfield (Integer). */ private final static String TAG_VALUE = "value"; /** * Property to indicate that the bits for this item should be drawn with thicker lines * (Boolean). */ private final static String TAG_BOLD = "bold"; /** * Used by the ToolTipListener to pass the current item to the * <code>Shell</code< showing the ToolTip (TreeItem). */ private final static String TAG_TREEITEM = "treeitem"; /** * Constructs a new instance of this class given its parent and a style value describing its * behavior and appearance. * <p> * The style value is either one of the style constants defined in class SWT which is applicable * to instances of this class, or must be built by bitwise OR'ing together (that is, using the * int "|" operator) two or more of those SWT style constants. The class description lists the * style constants that are applicable to the class. Style bits are also inherited from * superclasses. * * @see SWT#BORDER * * @param parent * a composite control which will be the parent of the new instance (cannot be null) * * @param style * the style of control to construct * @throws IllegalArgumentException * <ul> * <li> ERROR_NULL_ARGUMENT - if the parent is null </li> * </ul> * @throws SWTException * <ul> * <li> ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the * parent</li> <li> ERROR_INVALID_SUBCLASS - if this class is not an allowed * subclass </li> * </ul> */ public FuseBytePreviewControl(Composite parent, int style) { super(parent, style); GridLayout layout = new GridLayout(1, false); setLayout(layout); // The label at the top. fHeaderLabel = new Label(this, SWT.NONE); fHeaderLabel.setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false)); Composite treecomposite = new Composite(this, SWT.NONE); treecomposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); // The content Tree fTree = new Tree(treecomposite, SWT.FULL_SELECTION | SWT.H_SCROLL | SWT.V_SCROLL); fTree.setHeaderVisible(true); fTree.setBackground(this.getBackground()); fTree.setLinesVisible(true); // Add the listeners required for the bits column Listener paintlistener = new PaintListener(); fTree.addListener(SWT.MeasureItem, paintlistener); fTree.addListener(SWT.PaintItem, paintlistener); fTree.addListener(SWT.EraseItem, paintlistener); // Add the listeners required for the (fake) tooltips Listener tooltiplistener = new ToolTipListener(); fTree.addListener(SWT.Dispose, tooltiplistener); fTree.addListener(SWT.KeyDown, tooltiplistener); fTree.addListener(SWT.MouseMove, tooltiplistener); fTree.addListener(SWT.MouseHover, tooltiplistener); // We have three columns for: Name, Value (as Text) and Value (as single bits) final TreeColumn namecolumn = new TreeColumn(fTree, SWT.LEFT); namecolumn.setWidth(100); namecolumn.setText(COLUMN_HEADER_NAMES[fShortName ? 0 : 1]); namecolumn.setResizable(true); namecolumn.addSelectionListener(new SelectionAdapter() { // Toggle between short and long content for the name column @Override public void widgetSelected(SelectionEvent e) { fShortName = !fShortName; namecolumn.setText(COLUMN_HEADER_NAMES[fShortName ? 0 : 1]); updateTree(fCurrentValues); } }); final TreeColumn valuecolumn = new TreeColumn(fTree, SWT.LEFT); valuecolumn.setWidth(100); valuecolumn.setText(COLUMN_HEADER_VALUES[fShortValue ? 0 : 1]); valuecolumn.setResizable(true); valuecolumn.addSelectionListener(new SelectionAdapter() { // Toggle between short and long content for the values column @Override public void widgetSelected(SelectionEvent e) { fShortValue = !fShortValue; valuecolumn.setText(COLUMN_HEADER_VALUES[fShortValue ? 0 : 1]); updateTree(fCurrentValues); } }); TreeColumn bitscolumn = new TreeColumn(fTree, SWT.LEFT); bitscolumn.setWidth(100); bitscolumn.setText("Bits"); // bitscolumn.setResizable(true); // Set the Layout TreeItem item = new TreeItem(fTree, SWT.NONE); TreeColumnLayout treelayout = new TreeColumnLayout(); treecomposite.setLayout(treelayout); treelayout.setColumnData(namecolumn, new ColumnWeightData(35)); treelayout.setColumnData(valuecolumn, new ColumnWeightData(65)); treelayout.setColumnData(bitscolumn, new ColumnPixelData(item.getBounds().height * 8 + 2, false, true)); } /** * Sets the ByteValues Objects whose contents are shown in the control. * <p> * If the <code>newvalues</code> parameter is <code>null</code> then the control is blanked * except for a short message at the top. * </p> * * @param newvalues * <code>ByteValues</code> Object or <code>null</code> */ public void setByteValues(ByteValues newvalues) { if (newvalues == null) { clearTree(); fHeaderLabel.setText("No values set"); return; } fCurrentValues = newvalues; String type = newvalues.getType().toString(); String mcu = AVRMCUidConverter.id2name(newvalues.getMCUId()); String header = MessageFormat.format("{0} {1} preview", mcu, type); fHeaderLabel.setText(header); // Check if the mcu has changed. // If yes refill the complete table, otherwise only update the bitfields. if (!newvalues.getMCUId().equals(fMCUid)) { reloadTree(newvalues); fMCUid = newvalues.getMCUId(); } else { updateTree(newvalues); } colorTree(newvalues); // Expand all tree items and repack the columns so that always all information is // shown (even if this means that the scrollbars have to be used). for (TreeItem item : fRootItems) { item.setExpanded(true); } fTree.getColumn(COLUMN_BITS).pack(); // force redraw of the Bits column } /** * Clear the current tree and refill it with the content of the <code>ByteValues</code> * * @param newvalues * Valid <code>ByteValues</code> object. */ private void reloadTree(ByteValues newvalues) { clearTree(); // First we get all bytes and create the root tree items, one for each byte. int[] values = newvalues.getValues(); int i = 0; for (int value : values) { String bytename = newvalues.getByteName(i); if (bytename == null) { bytename = "RESERVED"; } TreeItem byteitem = new TreeItem(fTree, SWT.NONE); byteitem.setData(TAG_VALUE, newvalues.getValue(i)); byteitem.setData(TAG_BOLD, true); byteitem.setText(COLUMN_NAME, bytename); byteitem.setText(COLUMN_VALUE, toHex(value)); byteitem.setText(COLUMN_BITS, ""); byteitem.setFont(fBoldDialogFont); fRootItems.add(byteitem); i++; } // Now we get a list of all bitfields that the ByteValues object has and // add them to the tree as well. List<BitFieldDescription> alldescriptions = newvalues.getBitfieldDescriptions(); // Sort the bitfield descriptions according to their masks in descending order; Collections.sort(alldescriptions, new Comparator<BitFieldDescription>() { public int compare(BitFieldDescription o1, BitFieldDescription o2) { int mask1 = o1.getMask(); int mask2 = o2.getMask(); return mask2 - mask1; } }); for (BitFieldDescription bitfield : alldescriptions) { int byteindex = bitfield.getIndex(); TreeItem parent = fRootItems.get(byteindex); TreeItem newitem = new TreeItem(parent, SWT.NONE); newitem.setFont(fDialogFont); newitem.setData(TAG_BITFIELDDESCRIPTION, bitfield); newitem.setData(TAG_VALUE, newvalues.getValue(byteindex)); String name = bitfield.getName(); String desc = bitfield.getDescription(); newitem.setText(COLUMN_NAME, fShortName ? name : desc); String valuetext = newvalues.getNamedValueText(name); int value = newvalues.getNamedValue(name); newitem.setText(COLUMN_VALUE, fShortValue ? toHex(value) : valuetext); } } /** * Update the tree with the content from the ByteValues object. * <p> * The newvalues object must have the same MCU id as the currently displayed one, otherwise this * method will fail. * </p> * * @param newvalues * Valid <code>ByteValues</code> object. */ private void updateTree(ByteValues newvalues) { int[] values = newvalues.getValues(); // Iterate through all tree items and change the value of each item to the new values. for (int i = 0; i < fRootItems.size(); i++) { int value = values[i]; TreeItem byteitem = fRootItems.get(i); byteitem.setText(COLUMN_VALUE, toHex(value)); byteitem.setData(TAG_VALUE, value); TreeItem[] bitfielditems = byteitem.getItems(); for (TreeItem item : bitfielditems) { item.setData(TAG_VALUE, value); BitFieldDescription bfd = (BitFieldDescription) item .getData(TAG_BITFIELDDESCRIPTION); String name = bfd.getName(); int bitfieldvalue = newvalues.getNamedValue(name); String valuetext = newvalues.getNamedValueText(name); item.setText(COLUMN_NAME, fShortName ? name : bfd.getDescription()); item.setText(COLUMN_VALUE, fShortValue ? toHex(bitfieldvalue) : valuetext); } } } /** * Color the BitFields in the tree according to their {@link ConversionStatus}. * <p> * The colors used are: * <ul> * <li><em>Green</em>: The BitField was successfully converted</li> * <li><em>Yellow</em>: The BitField was converted, but the new value is different</li> * <li><em>Red</em>: The BitField did not exist in the source MCU and was set to the default * value</li> * <li><em>Black</em>: The BitField has been modified after the conversion.</li> * </ul> * * @param values * The <code>ByteValues</code> after the conversion. */ private void colorTree(ByteValues values) { // Get the colors used to indicate a conversion result final Color colorBlack = fTree.getDisplay().getSystemColor(SWT.COLOR_LIST_FOREGROUND); final Color colorRed = fTree.getDisplay().getSystemColor(SWT.COLOR_DARK_RED); final Color colorYellow = fTree.getDisplay().getSystemColor(SWT.COLOR_DARK_YELLOW); final Color colorGreen = fTree.getDisplay().getSystemColor(SWT.COLOR_DARK_GREEN); // Iterate though all BitField items in the tree for (TreeItem byteitem : fRootItems) { for (TreeItem bitfielditem : byteitem.getItems()) { BitFieldDescription bfd = (BitFieldDescription) bitfielditem .getData(TAG_BITFIELDDESCRIPTION); String bitfieldname = bfd.getName(); ConversionStatus status = values.getConversionStatus(bitfieldname); switch (status) { case SUCCESS: bitfielditem.setForeground(colorGreen); break; case VALUE_CHANGED: bitfielditem.setForeground(colorYellow); break; case NOT_IN_SOURCE: case NOT_IN_TARGET: bitfielditem.setForeground(colorRed); break; case MODIFIED: case NO_CONVERSION: case UNKNOWN: default: bitfielditem.setForeground(colorBlack); } } } } /** * Clear the currently shown content and reset the control. */ private void clearTree() { fTree.removeAll(); fRootItems.clear(); fMCUid = null; } /** * Format the given integer to a String with the format "0xXX". * <p> * Unlike the normal <code>Integer.toHexString(i)</code> method, this method will always produce * two digits, even with the high nibble at zero, and will output the hex value in uppercase. * This should make the value more readable than the standard <code>Integer.toHexString</code> * output. * </p> * <p> * If the given value is <code>-1</code>, then "n/a" is returned. * </p> * * @param value * Single byte value * @return String with the byte value as "0xXX" */ private static String toHex(int value) { if (value == -1) { return "n/a"; } String hex = "00" + Integer.toHexString(value); return "0x" + hex.substring(hex.length() - 2).toUpperCase(); } /** * This Class handles the drawing of the bits in the <code>COLUMN_BITS</code> column. * <p> * It is a listener that must be registered for three events: {@link SWT#MeasureItem}, * {@link SWT#PaintItem} and {@link SWT#EraseItem} * </p> */ private class PaintListener implements Listener { public void handleEvent(Event event) { // ignore all columns except the Bits column if (event.index != COLUMN_BITS) { return; } switch (event.type) { case SWT.MeasureItem: measureEvent(event); break; case SWT.PaintItem: paintEvent(event); break; case SWT.EraseItem: eraseEvent(event); break; } } /** * Handle a measure event. * <p> * Sets the width of the bits cell to its height * 8 (for the 8 bits). * </p> * * @param event */ private void measureEvent(Event event) { event.height = Math.max(event.height, 8); event.width = event.height * 8 + 2; } /** * Handle the paint event. * <p> * Gets the meta data from the tree item properties and draws 8 square bit representations. * </p> * * @param event */ private void paintEvent(Event event) { final TreeItem item = (TreeItem) event.item; BitFieldDescription bfd = (BitFieldDescription) item.getData(TAG_BITFIELDDESCRIPTION); Integer value = (Integer) item.getData(TAG_VALUE); Boolean bold = (Boolean) item.getData(TAG_BOLD); // Set defaults if any tag was missing int mask = 0xff; if (bfd != null) { mask = bfd.getMask(); } if (value == null) value = 0x00; if (bold == null) bold = false; // Get the Graphics Context and some colors final GC gc = event.gc; final Color foreground = gc.getForeground(); final Color background = gc.getBackground(); final Color colorGray = fTree.getDisplay().getSystemColor(SWT.COLOR_GRAY); final Color colorBlack = fTree.getDisplay().getSystemColor(SWT.COLOR_LIST_FOREGROUND); // Now we can iterate over the 8 bits. The counter i increment in drawing order�(left to // right), so below we use 7-i to get the actual bit value (right to left). for (int i = 0; i < 8; i++) { // Calculate the area for the single bit. This is used as reference for all // drawings. Rectangle bitarea = new Rectangle(event.x + i * event.height, event.y + 1, event.height - 3, event.height - 3); // determine if the current bit is inside of the mask boolean insidemask = (mask & (1 << (7 - i))) != 0; if (insidemask) { // bit inside of the mask, draw a box around the bit (bold if requested) gc.setForeground(colorBlack); gc.drawRectangle(bitarea); if (bold) { gc.drawRectangle(bitarea.x + 1, bitarea.y + 1, bitarea.width - 2, bitarea.height - 2); } } // Now for the content: three states are defined: // 1. invalid value or outside of mask: small dot // 2. inside of mask // 2a. 1-bit: filled box // 2b. 0-bit: empty box if ((value == -1) || !insidemask) { // invalid or outside of mask // draw a small square dot gc.setBackground(colorGray); int x = bitarea.x + event.height / 2 - event.height / 8; int y = bitarea.y + event.height / 2 - event.height / 8; gc.fillRectangle(x, y, event.height / 4, event.height / 4); } else { // bit inside of mask int x = bitarea.x + 2; int y = bitarea.y + 2; int width = bitarea.width - 3; int height = bitarea.height - 3; if ((value & (1 << (7 - i))) != 0) { // 1-bit inside of the mask // draw a large square dot gc.setBackground(colorGray); gc.fillRectangle(x, y, width, height); } else { // 0-bit inside of mask gc.setBackground(background); gc.fillRectangle(x, y, width, height); } } } // Restore the colors gc.setForeground(background); gc.setBackground(foreground); } /** * Handle the erase event. * * @param event */ private void eraseEvent(Event event) { // We just tell SWT that we draw the foreground ourself // (and let SWT fill the background) event.detail &= ~SWT.FOREGROUND; } } /** * Handles <code>SWT.MouseDown</code> events for the small ToolTip shell window. * <p> * It passes the event to the underlying tree item so that the tree control does not loose focus * when the user clicks on the ToolTip. * </p> * */ private class ToolTipShellListener implements Listener { public void handleEvent(Event event) { Label label = (Label) event.widget; Shell shell = label.getShell(); switch (event.type) { case SWT.MouseDown: Event e = new Event(); e.item = (TreeItem) label.getData(TAG_TREEITEM); // Set the selection as if the mouse down event went through to the table fTree.setSelection(new TreeItem[] { (TreeItem) e.item }); fTree.notifyListeners(SWT.Selection, e); shell.dispose(); break; case SWT.MouseExit: shell.dispose(); break; } } } /** * This class creates fake tooltips for the bitfield descriptions. * <p> * Once the mouse pointer "hovers" over a bitfield description, a tooltip is shown with the * bitfield name. The tooltip is placed at the right edge of the name/description column, * directly adjacent to the value. * </p> * The bitfield name is taken from the {@link FuseBytePreviewControl#TAG_NAME} property of the * tree item. </p> * */ private class ToolTipListener implements Listener { // This code is based on Snippet125 from: // http://www.java2s.com/Code/Java/SWT-JFace-Eclipse/CreatefaketooltipsforitemsinaSWTtable.htm /** The shell that will take our ToolTip */ Shell tip = null; /** The content for the ToolTip */ Label label = null; /* * (non-Javadoc) * @see org.eclipse.swt.widgets.Listener#handleEvent(org.eclipse.swt.widgets.Event) */ public void handleEvent(Event event) { switch (event.type) { // most events will cause disposal of an visible ToolTip window case SWT.Dispose: case SWT.KeyDown: case SWT.MouseMove: { if (tip == null) break; tip.dispose(); tip = null; label = null; break; } case SWT.MouseHover: { // Get the item over which the mouse hovers TreeItem item = fTree.getItem(new Point(event.x, event.y)); if (item == null) { break; } // No ToolTips for the root items if (item.getParentItem() == null) { break; } // Calculate the column. // Accumulate the column widths until the mouse.x lies within the column. // Both columnStart and index are then used further down TreeColumn[] columns = fTree.getColumns(); int columnStart = 0; int index = 0; for (index = 0; index < columns.length; index++) { if (event.x < columnStart + columns[index].getWidth()) { break; } columnStart += columns[index].getWidth(); } if (index == COLUMN_BITS) { // No ToolTips for the BITS column break; } if (tip != null && !tip.isDisposed()) { // dispose an already existing ToolTip window so we don't leave any // undisposed zombie ToolTip windows behind once we create a new one. tip.dispose(); } tip = new Shell(fTree.getShell(), SWT.ON_TOP | SWT.TOOL); tip.setLayout(new FillLayout()); label = new Label(tip, SWT.NONE); label.setForeground(fTree.getDisplay() .getSystemColor(SWT.COLOR_INFO_FOREGROUND)); label.setBackground(fTree.getDisplay() .getSystemColor(SWT.COLOR_INFO_BACKGROUND)); BitFieldDescription bfd = (BitFieldDescription) item .getData(TAG_BITFIELDDESCRIPTION); if (index == COLUMN_NAME) { label.setText(!fShortName ? bfd.getName() : bfd.getDescription()); } else { Integer value = (Integer) item.getData(TAG_VALUE); if (value != null && value != -1) { int bitfieldvalue = bfd.byteToBitField(value); label.setText(!fShortValue ? toHex(bitfieldvalue) : bfd .getValueText(bitfieldvalue)); } else { label.setText("undefined"); } } // Add a listener to the ToolTip so that any mouse clicks can be passed // through the ToolTip to the TreeItem below it. This prevents the loss of // focus if a user decides to click on the ToolTip. Listener listener = new ToolTipShellListener(); label.addListener(SWT.MouseExit, listener); label.addListener(SWT.MouseDown, listener); label.setData(TAG_TREEITEM, item); // Now determine the position of the ToolTip. // In this case I use not the standard mouse cursor position. // Instead the ToolTip is shown aligned with the left edge of // the current cell. // This is non-standard but looks quite good and serves the purpose. Point size = tip.computeSize(SWT.DEFAULT, SWT.DEFAULT); Rectangle rect = item.getBounds(0); if (index == COLUMN_NAME) { Point pt = fTree.toDisplay(rect.x + rect.width, rect.y); tip.setBounds(pt.x - size.x, pt.y, size.x, size.y); } else { Point pt = fTree.toDisplay(columnStart, rect.y); tip.setBounds(pt.x, pt.y, size.x, size.y); } // After all the setup we can finally show the ToolTip :-) tip.setVisible(true); } } } } }