/******************************************************************************* * Copyright (c) 2011 Google, Inc. * 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: * Google, Inc. - initial API and implementation *******************************************************************************/ package org.eclipse.wb.internal.core.model.property.table; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.ISelectionProvider; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.swt.SWT; import org.eclipse.swt.events.KeyAdapter; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.MouseAdapter; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseMoveListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.ScrollBar; import org.eclipse.wb.draw2d.IColorConstants; import org.eclipse.wb.draw2d.ICursorConstants; import org.eclipse.wb.internal.core.DesignerPlugin; import org.eclipse.wb.internal.core.EnvironmentUtils; import org.eclipse.wb.internal.core.model.property.Property; import org.eclipse.wb.internal.core.model.property.category.PropertyCategory; import org.eclipse.wb.internal.core.model.property.category.PropertyCategoryProvider; import org.eclipse.wb.internal.core.model.property.category.PropertyCategoryProviders; import org.eclipse.wb.internal.core.model.property.editor.PropertyEditor; import org.eclipse.wb.internal.core.model.property.editor.complex.IComplexPropertyEditor; import org.eclipse.wb.internal.core.model.property.editor.presentation.ButtonPropertyEditorPresentation; import org.eclipse.wb.internal.core.model.property.editor.presentation.PropertyEditorPresentation; import org.eclipse.wb.internal.core.utils.check.Assert; import org.eclipse.wb.internal.core.utils.ui.DrawUtils; import java.util.Collection; import java.util.List; import java.util.Set; /** * Control that can display {@link Property}'s and edit them using {@link PropertyEditor}'s. * * @author scheglov_ke * @author lobas_av * @coverage core.model.property.table */ public class PropertyTable extends Canvas implements ISelectionProvider { //////////////////////////////////////////////////////////////////////////// // // Colors // //////////////////////////////////////////////////////////////////////////// private static final Color COLOR_BACKGROUND = IColorConstants.listBackground; private static final Color COLOR_NO_PROPERTIES = IColorConstants.gray; private static final Color COLOR_LINE = IColorConstants.lightGray; private static final Color COLOR_COMPLEX_LINE = DrawUtils.getShiftedColor( IColorConstants.lightGray, -32); private static final Color COLOR_PROPERTY_BG = DrawUtils.getShiftedColor(COLOR_BACKGROUND, -12); private static final Color COLOR_PROPERTY_BG_MODIFIED = COLOR_BACKGROUND; private static final Color COLOR_PROPERTY_FG_TITLE = IColorConstants.listForeground; private static final Color COLOR_PROPERTY_FG_VALUE = DrawUtils.isDarkColor(IColorConstants.listBackground) ? IColorConstants.lightBlue : IColorConstants.darkBlue; private static final Color COLOR_PROPERTY_BG_SELECTED = IColorConstants.listSelection; private static final Color COLOR_PROPERTY_FG_SELECTED = IColorConstants.listSelectionText; private static final Color COLOR_PROPERTY_FG_ADVANCED = IColorConstants.gray; // BEGIN ADT MODIFICATIONS public static final Color COLOR_PROPERTY_FG_DEFAULT = IColorConstants.darkGray; // END ADT MODIFICATIONS //////////////////////////////////////////////////////////////////////////// // // Sizes // //////////////////////////////////////////////////////////////////////////// private static final int MIN_COLUMN_WIDTH = 75; private static final int MARGIN_LEFT = 2; private static final int MARGIN_RIGHT = 1; private static final int STATE_IMAGE_MARGIN_RIGHT = 4; //////////////////////////////////////////////////////////////////////////// // // Images // //////////////////////////////////////////////////////////////////////////// private static final Image m_plusImage = DesignerPlugin.getImage("properties/plus.gif"); private static final Image m_minusImage = DesignerPlugin.getImage("properties/minus.gif"); private static int m_stateWidth = 9; //////////////////////////////////////////////////////////////////////////// // // Instance fields // //////////////////////////////////////////////////////////////////////////// private final PropertyTableTooltipHelper m_tooltipHelper; private boolean m_showAdvancedProperties; private Property[] m_rawProperties; private List m_properties; private final Set m_expandedIds = Sets.newTreeSet(); // BEGIN ADT MODIFICATIONS // Add support for "expand by default" : If you specify ids to be collapsed by // default, then any *other* ids will be expanded by default. private Set m_collapsedIds = null; /** * Sets a set of ids that should be collapsed by default. Everything else * will be expanded by default. If this method is not called, ids are * collapsed by default instead. * * @param names set of ids to be collapsed */ public void setDefaultCollapsedNames(Collection names) { m_collapsedIds = Sets.newTreeSet(); for (String name : names) { m_collapsedIds.add("|" + name); // See PropertyInfo constructor for id syntax } } // END ADT MODIFICATIONS private Image m_bufferedImage; private int m_rowHeight; private int m_selection; private int m_page; private int m_splitter = -1; //////////////////////////////////////////////////////////////////////////// // // Constructor // //////////////////////////////////////////////////////////////////////////// public PropertyTable(Composite parent, int style) { super(parent, style | SWT.V_SCROLL | SWT.NO_BACKGROUND | SWT.NO_REDRAW_RESIZE); hookControlEvents(); // calculate sizes { GC gc = new GC(this); try { m_rowHeight = 1 + gc.getFontMetrics().getHeight() + 1; } finally { gc.dispose(); } } // install tooltip helper m_tooltipHelper = new PropertyTableTooltipHelper(this); } //////////////////////////////////////////////////////////////////////////// // // Events // //////////////////////////////////////////////////////////////////////////// /** * Adds listeners for events. */ private void hookControlEvents() { addListener(SWT.Dispose, new Listener() { @Override public void handleEvent(Event event) { disposeBufferedImage(); } }); addListener(SWT.Resize, new Listener() { @Override public void handleEvent(Event event) { handleResize(); } }); addListener(SWT.Paint, new Listener() { @Override public void handleEvent(Event event) { handlePaint(event.gc, event.x, event.y, event.width, event.height); } }); getVerticalBar().addListener(SWT.Selection, new Listener() { @Override public void handleEvent(Event event) { handleVerticalScrolling(); } }); addMouseListener(new MouseAdapter() { @Override public void mouseDown(MouseEvent event) { forceFocus(); handleMouseDown(event); } @Override public void mouseUp(MouseEvent event) { handleMouseUp(event); } @Override public void mouseDoubleClick(MouseEvent event) { handleMouseDoubleClick(event); } }); addMouseMoveListener(new MouseMoveListener() { @Override public void mouseMove(MouseEvent event) { handleMouseMove(event); } }); // keyboard addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { handleKeyDown(e); } }); } //////////////////////////////////////////////////////////////////////////// // // Events: dispose, resize, scroll // //////////////////////////////////////////////////////////////////////////// /** * Disposes image used for double buffered painting. */ private void disposeBufferedImage() { if (m_bufferedImage != null) { m_bufferedImage.dispose(); m_bufferedImage = null; } } /** * Handles {@link SWT#Resize} event. */ private void handleResize() { disposeBufferedImage(); configureScrolling(); // splitter { // set default value for splitter if (m_splitter <= MIN_COLUMN_WIDTH) { m_splitter = Math.max((int) (getClientArea().width * 0.4), MIN_COLUMN_WIDTH); } configureSplitter(); } } /** * Handles {@link SWT#Selection} event for vertical {@link ScrollBar}. */ private void handleVerticalScrolling() { ScrollBar verticalBar = getVerticalBar(); if (verticalBar.getEnabled()) { // update selection m_selection = verticalBar.getSelection(); // redraw (but not include vertical bar to avoid flashing) { Rectangle clientArea = getClientArea(); redraw(clientArea.x, clientArea.y, clientArea.width, clientArea.height, false); } } } //////////////////////////////////////////////////////////////////////////// // // Keyboard // //////////////////////////////////////////////////////////////////////////// /** * Handles {@link SWT#KeyDown} event. */ private void handleKeyDown(KeyEvent e) { if (m_activePropertyInfo != null) { try { Property property = m_activePropertyInfo.getProperty(); // expand/collapse if (m_activePropertyInfo.isComplex()) { if (!m_activePropertyInfo.isExpanded() && (e.character == '+' || e.keyCode == SWT.ARROW_RIGHT)) { m_activePropertyInfo.expand(); configureScrolling(); return; } if (m_activePropertyInfo.isExpanded() && (e.character == '-' || e.keyCode == SWT.ARROW_LEFT)) { m_activePropertyInfo.collapse(); configureScrolling(); return; } } // navigation if (navigate(e)) { return; } // editor activation if (e.character == ' ' || e.character == SWT.CR) { activateEditor(property, null); return; } // DEL if (e.keyCode == SWT.DEL) { e.doit = false; property.setValue(Property.UNKNOWN_VALUE); return; } // send to editor property.getEditor().keyDown(this, property, e); } catch (Throwable ex) { DesignerPlugin.log(ex); } } } /** * @return true if given {@link KeyEvent} was navigation event, so new * {@link PropertyInfo} was selected. */ public boolean navigate(KeyEvent e) { int index = m_properties.indexOf(m_activePropertyInfo); Rectangle clientArea = getClientArea(); // int newIndex = index; if (e.keyCode == SWT.HOME) { newIndex = 0; } else if (e.keyCode == SWT.END) { newIndex = m_properties.size() - 1; } else if (e.keyCode == SWT.PAGE_UP) { newIndex = Math.max(index - m_page + 1, 0); } else if (e.keyCode == SWT.PAGE_DOWN) { newIndex = Math.min(index + m_page - 1, m_properties.size() - 1); } else if (e.keyCode == SWT.ARROW_UP) { newIndex = Math.max(index - 1, 0); } else if (e.keyCode == SWT.ARROW_DOWN) { newIndex = Math.min(index + 1, m_properties.size() - 1); } // activate new property if (newIndex != index && newIndex < m_properties.size()) { setActivePropertyInfo(m_properties.get(newIndex)); // check for scrolling int y = m_rowHeight * (newIndex - m_selection); if (y < 0) { m_selection = newIndex; configureScrolling(); } else if (y + m_rowHeight > clientArea.height) { m_selection = newIndex - m_page + 1; configureScrolling(); } // repaint redraw(); return true; } // no navigation change return false; } //////////////////////////////////////////////////////////////////////////// // // Events: mouse // //////////////////////////////////////////////////////////////////////////// private boolean m_splitterResizing; /** * We do expand/collapse on to events: click on state sign and on double click. But when we double * click on state sign, we will have two events, so we should ignore double click. */ private long m_lastExpandCollapseTime; /** * Handles {@link SWT#MouseDown} event. */ private void handleMouseDown(MouseEvent event) { m_splitterResizing = event.button == 1 && m_properties != null && isLocationSplitter(event.x); // click in property if (!m_splitterResizing && m_properties != null) { int propertyIndex = getPropertyIndex(event.y); if (propertyIndex >= m_properties.size()) { return; } // prepare property setActivePropertyInfo(m_properties.get(propertyIndex)); Property property = m_activePropertyInfo.getProperty(); // de-activate current editor deactivateEditor(true); redraw(); // activate editor if (isLocationValue(event.x)) { activateEditor(property, getValueRelativeLocation(event.x, event.y)); } } } /** * Handles {@link SWT#MouseUp} event. */ private void handleMouseUp(MouseEvent event) { if (event.button == 1) { // resize splitter if (m_splitterResizing) { m_splitterResizing = false; return; } // if out of bounds, then ignore if (!getClientArea().contains(event.x, event.y)) { return; } // update if (m_properties != null) { int index = getPropertyIndex(event.y); if (index < m_properties.size()) { PropertyInfo propertyInfo = m_properties.get(index); // check for expand/collapse if (isLocationState(propertyInfo, event.x)) { try { m_lastExpandCollapseTime = System.currentTimeMillis(); propertyInfo.flip(); configureScrolling(); } catch (Throwable e) { DesignerPlugin.log(e); } } } } } } /** * Handles {@link SWT#MouseDoubleClick} event. */ private void handleMouseDoubleClick(MouseEvent event) { if (System.currentTimeMillis() - m_lastExpandCollapseTime > getDisplay().getDoubleClickTime()) { try { if (m_activePropertyInfo != null) { if (m_activePropertyInfo.isComplex()) { m_activePropertyInfo.flip(); configureScrolling(); } else { Property property = m_activePropertyInfo.getProperty(); property.getEditor().doubleClick(property, getValueRelativeLocation(event.x, event.y)); } } } catch (Throwable e) { handleException(e); } } } /** * Handles {@link SWT#MouseMove} event. */ private void handleMouseMove(MouseEvent event) { int x = event.x; // resize splitter if (m_splitterResizing) { m_splitter = x; configureSplitter(); redraw(); return; } // if out of bounds, then ignore if (!getClientArea().contains(event.x, event.y)) { return; } // update if (m_properties != null) { // update cursor if (isLocationSplitter(x)) { setCursor(ICursorConstants.SIZEWE); } else { setCursor(null); } // update tooltip helper updateTooltip(event); } else { updateTooltipNoProperty(); } } /** * Updates {@link PropertyTableTooltipHelper}. */ private void updateTooltip(MouseEvent event) { int x = event.x; int propertyIndex = getPropertyIndex(event.y); // if (propertyIndex < m_properties.size()) { PropertyInfo propertyInfo = m_properties.get(propertyIndex); Property property = propertyInfo.getProperty(); int y = (propertyIndex - m_selection) * m_rowHeight; // check for title { int titleX = getTitleTextX(propertyInfo); int titleRight = m_splitter - 2; if (titleX <= x && x < titleRight) { m_tooltipHelper.update(property, true, false, titleX, titleRight, y, m_rowHeight); return; } } // check for value { int valueX = m_splitter + 3; if (x > valueX) { m_tooltipHelper.update( property, false, true, valueX, getClientArea().width, y, m_rowHeight); } } } else { updateTooltipNoProperty(); } } private void updateTooltipNoProperty() { m_tooltipHelper.update(null, false, false, 0, 0, 0, 0); } //////////////////////////////////////////////////////////////////////////// // // Editor // //////////////////////////////////////////////////////////////////////////// private PropertyInfo m_activePropertyInfo; private String m_activePropertyId; private PropertyEditor m_activeEditor; /** * Tries to activate editor for {@link PropertyInfo} under cursor. * * @param location * the mouse location, if editor is activated using mouse click, or null if * it is activated using keyboard. */ public void activateEditor(Property property, Point location) { try { // de-activate old editor deactivateEditor(true); // activate editor PropertyEditor editor = property.getEditor(); try { if (editor.activate(this, property, location)) { m_activeEditor = editor; } } catch (Throwable e) { handleException(e); } // set bounds setActiveEditorBounds(); } catch (NullPointerException e) { if (EnvironmentUtils.IS_MAC) { // Workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=388574 PropertyEditor editor = property.getEditor(); PropertyEditorPresentation presentation = editor.getPresentation(); if (presentation instanceof ButtonPropertyEditorPresentation) { ButtonPropertyEditorPresentation button = (ButtonPropertyEditorPresentation) presentation; try { button.click(this, property); } catch (Exception ex) { deactivateEditor(false); handleException(e); } return; } } DesignerPlugin.log(e); } catch (Throwable e) { DesignerPlugin.log(e); } } /** * Deactivates current {@link PropertyEditor}. */ public void deactivateEditor(boolean save) { if (m_activeEditor != null) { PropertyEditor activeEditor = m_activeEditor; m_activeEditor = null; if (m_activePropertyInfo != null && m_activePropertyInfo.m_property != null) { activeEditor.deactivate(this, m_activePropertyInfo.m_property, save); } } } /** * Sets correct bounds for active editor, for example we need update bounds after scrolling. */ private void setActiveEditorBounds() { if (m_activeEditor != null) { int index = m_properties.indexOf(m_activePropertyInfo); if (index == -1) { // it is possible that active property was hidden because its parent was collapsed deactivateEditor(true); } else { // prepare bounds for editor Rectangle bounds; { Rectangle clientArea = getClientArea(); int x = m_splitter + 1; int width = clientArea.width - x - MARGIN_RIGHT; int y = m_rowHeight * (index - m_selection) + 1; int height = m_rowHeight - 1; bounds = new Rectangle(x, y, width, height); } // update bounds using presentation { PropertyEditorPresentation presentation = m_activeEditor.getPresentation(); if (presentation != null) { int presentationWidth = presentation.show( this, m_activePropertyInfo.m_property, bounds.x, bounds.y, bounds.width, bounds.height); bounds.width -= presentationWidth; } } // set editor bounds m_activeEditor.setBounds(bounds); } } } //////////////////////////////////////////////////////////////////////////// // // Exceptions // //////////////////////////////////////////////////////////////////////////// private IPropertyExceptionHandler m_exceptionHandler; /** * Sets {@link IPropertyExceptionHandler} for handling all exceptions. */ public void setExceptionHandler(IPropertyExceptionHandler exceptionHandler) { m_exceptionHandler = exceptionHandler; } /** * Handles given {@link Throwable}.
* Right now it just logs it, but in future we can open some dialog here. */ public void handleException(Throwable e) { m_exceptionHandler.handle(e); } //////////////////////////////////////////////////////////////////////////// // // Scrolling // //////////////////////////////////////////////////////////////////////////// /** * Configures vertical {@link ScrollBar}. */ private void configureScrolling() { ScrollBar verticalBar = getVerticalBar(); if (m_properties == null) { verticalBar.setEnabled(false); } else { m_page = getClientArea().height / m_rowHeight; m_selection = Math.max(0, Math.min(m_properties.size() - m_page, m_selection)); verticalBar.setValues(m_selection, 0, m_properties.size(), m_page, 1, m_page); // enable/disable scrolling if (m_properties.size() <= m_page) { verticalBar.setEnabled(false); } else { verticalBar.setEnabled(true); } } // redraw, we reconfigure scrolling only if list of properties was changed, so we should redraw redraw(); } //////////////////////////////////////////////////////////////////////////// // // Location/size utils // //////////////////////////////////////////////////////////////////////////// /** * @return the X position for first pixel of {@link PropertyInfo} title (location of * state image). */ private int getTitleX(PropertyInfo propertyInfo) { return MARGIN_LEFT + getLevelIndent() * propertyInfo.getLevel(); } /** * @return the X position for first pixel of {@link PropertyInfo} title text. */ private int getTitleTextX(PropertyInfo propertyInfo) { return getTitleX(propertyInfo) + getLevelIndent(); } /** * @return the indentation for single level. */ private int getLevelIndent() { return m_stateWidth + STATE_IMAGE_MARGIN_RIGHT; } /** * Checks horizontal splitter value to boundary values. */ private void configureSplitter() { Rectangle clientArea = getClientArea(); // check title width if (m_splitter < MIN_COLUMN_WIDTH) { m_splitter = MIN_COLUMN_WIDTH; } // check value width if (clientArea.width - m_splitter < MIN_COLUMN_WIDTH) { m_splitter = clientArea.width - MIN_COLUMN_WIDTH; } } /** * @return the index in {@link #m_properties} corresponding given y location. */ private int getPropertyIndex(int y) { return m_selection + y / m_rowHeight; } /** * @return true if given x coordinate is on state (plus/minus) image. */ private boolean isLocationState(PropertyInfo propertyInfo, int x) { int levelX = getTitleX(propertyInfo); return propertyInfo.isComplex() && levelX <= x && x <= levelX + m_stateWidth; } /** * Returns true if x coordinate is on splitter. */ private boolean isLocationSplitter(int x) { return Math.abs(m_splitter - x) < 2; } /** * @return true if given x is on value part of property. */ private boolean isLocationValue(int x) { return x > m_splitter + 2; } /** * @param x * the {@link PropertyTable} relative coordinate. * @param y * the {@link PropertyTable} relative coordinate. * * @return the location relative to the value part of property. */ private Point getValueRelativeLocation(int x, int y) { return new Point(x - (m_splitter + 2), y - m_rowHeight * getPropertyIndex(y)); } //////////////////////////////////////////////////////////////////////////// // // Access // //////////////////////////////////////////////////////////////////////////// /** * Shows or hides {@link Property}-s with {@link PropertyCategory#ADVANCED}. */ public void setShowAdvancedProperties(boolean showAdvancedProperties) { m_showAdvancedProperties = showAdvancedProperties; setInput0(); } /** * Sets the array of {@link Property}'s to display/edit. */ public void setInput(Property[] properties) { m_rawProperties = properties; setInput0(); } private void setInput0() { // send "hide" to all PropertyEditorPresentation's if (m_properties != null) { for (PropertyInfo propertyInfo : m_properties) { Property property = propertyInfo.getProperty(); // hide presentation { PropertyEditorPresentation presentation = property.getEditor().getPresentation(); if (presentation != null) { presentation.hide(this, property); } } } } // set new properties if (m_rawProperties == null || m_rawProperties.length == 0) { deactivateEditor(false); m_properties = Lists.newArrayList(); } else { try { // add PropertyInfo for each Property m_properties = Lists.newArrayList(); for (Property property : m_rawProperties) { if (rawProperties_shouldShow(property)) { PropertyInfo propertyInfo = new PropertyInfo(property); m_properties.add(propertyInfo); } } // expand properties using history while (true) { boolean expanded = false; List currentProperties = Lists.newArrayList(m_properties); for (PropertyInfo propertyInfo : currentProperties) { expanded |= propertyInfo.expandFromHistory(); } // stop if (!expanded) { break; } } } catch (Throwable e) { DesignerPlugin.log(e); } } // update active property if (m_activePropertyId != null) { PropertyInfo newActivePropertyInfo = null; // try to find corresponding PropertyInfo if (m_properties != null) { for (PropertyInfo propertyInfo : m_properties) { if (propertyInfo.m_id.equals(m_activePropertyId)) { newActivePropertyInfo = propertyInfo; break; } } } // set new PropertyInfo setActivePropertyInfo(newActivePropertyInfo); } // update scroll bar configureScrolling(); } /** * @return true if given {@link Property} should be displayed. */ private boolean rawProperties_shouldShow(Property property) throws Exception { PropertyCategory category = getCategory(property); // filter out hidden properties if (category.isHidden()) { return false; } // filter out advanced properties if (category.isAdvanced()) { if (!m_showAdvancedProperties && !property.isModified()) { return false; } } if (category.isAdvancedReally()) { return m_showAdvancedProperties; } // OK return true; } /** * Activates given {@link Property}. */ public void setActiveProperty(Property property) { for (PropertyInfo propertyInfo : m_properties) { if (propertyInfo.m_property == property) { setActivePropertyInfo(propertyInfo); break; } } } //////////////////////////////////////////////////////////////////////////// // // Access: only for testing // //////////////////////////////////////////////////////////////////////////// /** * @return the count of properties in "expanded" list. */ public int forTests_getPropertiesCount() { return m_properties.size(); } /** * @return the {@link Property} from "expanded" list. */ public Property forTests_getProperty(int index) { return m_properties.get(index).getProperty(); } /** * Expands the {@link PropertyInfo} with given index. */ public void forTests_expand(int index) throws Exception { m_properties.get(index).expand(); } /** * @return the position of splitter. */ public int forTests_getSplitter() { return m_splitter; } /** * @return the location of state image (plus/minus) for given {@link Property}. */ public Point forTests_getStateLocation(Property property) { PropertyInfo propertyInfo = getPropertyInfo(property); if (propertyInfo != null) { int index = m_properties.indexOf(propertyInfo); int x = getTitleX(propertyInfo); int y = m_rowHeight * (index - m_selection) + 1; return new Point(x, y); } return null; } /** * @return the location of state image (plus/minus) for given {@link Property}. */ public Point forTests_getValueLocation(Property property) { PropertyInfo propertyInfo = getPropertyInfo(property); if (propertyInfo != null) { int index = m_properties.indexOf(propertyInfo); int x = m_splitter + 5; int y = m_rowHeight * (index - m_selection) + 1; return new Point(x, y); } return null; } /** * @return the active {@link PropertyEditor}. */ public PropertyEditor forTests_getActiveEditor() { return m_activeEditor; } /** * @return the {@link PropertyCategory} that is used by this {@link PropertyTable} to display. */ public PropertyCategory forTests_getCategory(Property property) { return getCategory(property); } /** * @return the {@link PropertyInfo}for given {@link Property}. */ private PropertyInfo getPropertyInfo(Property property) { for (PropertyInfo propertyInfo : m_properties) { if (propertyInfo.getProperty() == property) { return propertyInfo; } } return null; } //////////////////////////////////////////////////////////////////////////// // // ISelectionProvider // //////////////////////////////////////////////////////////////////////////// private final List m_selectionListeners = Lists.newArrayList(); @Override public void addSelectionChangedListener(ISelectionChangedListener listener) { if (!m_selectionListeners.contains(listener)) { m_selectionListeners.add(listener); } } @Override public void removeSelectionChangedListener(ISelectionChangedListener listener) { m_selectionListeners.add(listener); } @Override public ISelection getSelection() { if (m_activePropertyInfo != null) { return new StructuredSelection(m_activePropertyInfo.getProperty()); } else { return StructuredSelection.EMPTY; } } @Override public void setSelection(ISelection selection) { throw new UnsupportedOperationException(); } /** * Sets the new active {@link PropertyInfo} and sends event to {@link ISelectionChangedListener} * 's. */ private void setActivePropertyInfo(PropertyInfo activePropertyInfo) { m_activePropertyInfo = activePropertyInfo; // update m_activePropertyId only when really select property, // not just remove selection because there are no corresponding property for old active // so, later for some other component, if we don't select other property, old active will be selected if (m_activePropertyInfo != null) { m_activePropertyId = m_activePropertyInfo.m_id; } // make sure that active property is visible if (m_activePropertyInfo != null) { int row = m_properties.indexOf(m_activePropertyInfo); if (m_selection <= row && row < m_selection + m_page) { } else { m_selection = row; configureScrolling(); } } // send events SelectionChangedEvent selectionEvent = new SelectionChangedEvent(this, getSelection()); for (ISelectionChangedListener listener : m_selectionListeners) { listener.selectionChanged(selectionEvent); } // re-draw redraw(); } //////////////////////////////////////////////////////////////////////////// // // Painting // //////////////////////////////////////////////////////////////////////////// private boolean m_painting; private Font m_baseFont; private Font m_boldFont; private Font m_italicFont; /** * Handles {@link SWT#Paint} event. */ private void handlePaint(GC gc, int x, int y, int width, int height) { // sometimes we disable Eclipse Shell to prevent user actions, but we do this for short time if (!isEnabled()) { return; } // prevent recursion if (m_painting) { return; } m_painting = true; // try { setActiveEditorBounds(); // prepare buffered image if (m_bufferedImage == null || m_bufferedImage.isDisposed()) { Point size = getSize(); m_bufferedImage = new Image(DesignerPlugin.getStandardDisplay(), size.x, size.y); } // prepare buffered GC GC bufferedGC = null; try { // perform some drawing { bufferedGC = new GC(m_bufferedImage); bufferedGC.setClipping(x, y, width, height); bufferedGC.setBackground(gc.getBackground()); bufferedGC.setForeground(gc.getForeground()); bufferedGC.setFont(gc.getFont()); bufferedGC.setLineStyle(gc.getLineStyle()); bufferedGC.setLineWidth(gc.getLineWidth()); } // fill client area { Rectangle clientArea = getClientArea(); bufferedGC.setBackground(COLOR_BACKGROUND); bufferedGC.fillRectangle(clientArea); } // draw content if (m_properties == null || m_properties.size() == 0) { drawEmptyContent(bufferedGC); } else { drawContent(bufferedGC); } } finally { // flush image if (bufferedGC != null) { bufferedGC.dispose(); } } gc.drawImage(m_bufferedImage, 0, 0); } finally { m_painting = false; } } /** * Draws content when there are no properties. */ private void drawEmptyContent(GC gc) { Rectangle area = getClientArea(); // draw message gc.setForeground(COLOR_NO_PROPERTIES); DrawUtils.drawStringCHCV( gc, "", 0, 0, area.width, area.height); } /** * Draws all {@link PropertyInfo}'s, separators, etc. */ private void drawContent(GC gc) { Rectangle clientArea = getClientArea(); // prepare fonts m_baseFont = gc.getFont(); m_boldFont = DrawUtils.getBoldFont(m_baseFont); m_italicFont = DrawUtils.getItalicFont(m_baseFont); // show presentations int[] presentationsWidth = showPresentations(clientArea); // draw properties { int y = clientArea.y - m_rowHeight * m_selection; for (int i = 0; i < m_properties.size(); i++) { // skip, if not visible yet if (y + m_rowHeight < 0) { y += m_rowHeight; continue; } // stop, if already invisible if (y > clientArea.height) { break; } // draw single property { PropertyInfo propertyInfo = m_properties.get(i); drawProperty(gc, propertyInfo, y + 1, m_rowHeight - 1, clientArea.width - presentationsWidth[i]); y += m_rowHeight; } // draw row separator gc.setForeground(COLOR_LINE); gc.drawLine(0, y, clientArea.width, y); } } // draw expand line drawExpandLines(gc, clientArea); // draw rectangle around table gc.setForeground(COLOR_LINE); gc.drawRectangle(0, 0, clientArea.width - 1, clientArea.height - 1); // draw splitter gc.setForeground(COLOR_LINE); gc.drawLine(m_splitter, 0, m_splitter, clientArea.height); // dispose font m_boldFont.dispose(); m_italicFont.dispose(); } /** * Shows {@link PropertyEditorPresentation}'s for all {@link Property}'s, i.e. updates also their * bounds. So, some {@link PropertyEditorPresentation}'s become invisible because they are moved * above or below visible client area. * * @return the array of width for each {@link PropertyEditorPresentation}'s, consumed on the * right. */ private int[] showPresentations(Rectangle clientArea) { int[] presentationsWidth = new int[m_properties.size()]; // prepare value rectangle int x = m_splitter + 4; int w = clientArea.width - x - MARGIN_RIGHT; // show presentation's for all properties int y = clientArea.y - m_rowHeight * m_selection; for (int i = 0; i < m_properties.size(); i++) { PropertyInfo propertyInfo = m_properties.get(i); Property property = propertyInfo.getProperty(); PropertyEditorPresentation presentation = property.getEditor().getPresentation(); if (presentation != null) { presentationsWidth[i] = presentation.show(this, property, x, y + 1, w, m_rowHeight - 1); } y += m_rowHeight; } return presentationsWidth; } /** * Draws lines from expanded complex property to its last sub-property. */ private void drawExpandLines(GC gc, Rectangle clientArea) { int height = m_rowHeight - 1; int xOffset = m_plusImage.getBounds().width / 2; int yOffset = (height - m_plusImage.getBounds().width) / 2; // int y = clientArea.y - m_selection * m_rowHeight; gc.setForeground(COLOR_COMPLEX_LINE); for (int i = 0; i < m_properties.size(); i++) { PropertyInfo propertyInfo = m_properties.get(i); // if (propertyInfo.isExpanded()) { int index = m_properties.indexOf(propertyInfo); // prepare index of last sub-property int index2 = index; for (; index2 < m_properties.size(); index2++) { PropertyInfo nextPropertyInfo = m_properties.get(index2); if (nextPropertyInfo != propertyInfo && nextPropertyInfo.getLevel() <= propertyInfo.getLevel()) { break; } } index2--; // draw line if there are children if (index2 > index) { int x = getTitleX(propertyInfo) + xOffset; int y1 = y + height - yOffset; int y2 = y + m_rowHeight * (index2 - index) + m_rowHeight / 2; gc.drawLine(x, y1, x, y2); gc.drawLine(x, y2, x + m_rowHeight / 3, y2); } } // y += m_rowHeight; } } /** * Draws single {@link PropertyInfo} in specified rectangle. */ private void drawProperty(GC gc, PropertyInfo propertyInfo, int y, int height, int width) { // remember colors Color oldBackground = gc.getBackground(); Color oldForeground = gc.getForeground(); // draw property try { Property property = propertyInfo.getProperty(); boolean isActiveProperty = m_activePropertyInfo != null && m_activePropertyInfo.getProperty() == property; // set background boolean modified = property.isModified(); { if (isActiveProperty) { gc.setBackground(COLOR_PROPERTY_BG_SELECTED); } else { if (modified) { gc.setBackground(COLOR_PROPERTY_BG_MODIFIED); } else { gc.setBackground(COLOR_PROPERTY_BG); } } gc.fillRectangle(0, y, width, height); } // draw state image if (propertyInfo.isShowComplex()) { Image stateImage = propertyInfo.isExpanded() ? m_minusImage : m_plusImage; DrawUtils.drawImageCV(gc, stateImage, getTitleX(propertyInfo), y, height); } // draw title { // configure GC { gc.setForeground(COLOR_PROPERTY_FG_TITLE); // check category if (getCategory(property).isAdvanced()) { gc.setForeground(COLOR_PROPERTY_FG_ADVANCED); gc.setFont(m_italicFont); } else if (getCategory(property).isPreferred() || getCategory(property).isSystem()) { gc.setFont(m_boldFont); } // check for active if (isActiveProperty) { gc.setForeground(COLOR_PROPERTY_FG_SELECTED); } } // paint title int x = getTitleTextX(propertyInfo); DrawUtils.drawStringCV(gc, property.getTitle(), x, y, m_splitter - x, height); } // draw value { // configure GC gc.setFont(m_baseFont); if (!isActiveProperty) { gc.setForeground(COLOR_PROPERTY_FG_VALUE); } // prepare value rectangle int x = m_splitter + 4; int w = width - x - MARGIN_RIGHT; // paint value // BEGIN ADT MODIFICATIONS if (!modified) { gc.setForeground(COLOR_PROPERTY_FG_DEFAULT); } // END ADT MODIFICATIONS property.getEditor().paint(property, gc, x, y, w, height); } } catch (Throwable e) { DesignerPlugin.log(e); } finally { // restore colors gc.setBackground(oldBackground); gc.setForeground(oldForeground); } } //////////////////////////////////////////////////////////////////////////// // // PropertyCategory // //////////////////////////////////////////////////////////////////////////// private PropertyCategoryProvider m_propertyCategoryProvider = PropertyCategoryProviders.fromProperty(); /** * Sets the {@link PropertyCategoryProvider} that can be used to tweak usual * {@link PropertyCategory}. */ public void setPropertyCategoryProvider(PropertyCategoryProvider propertyCategoryProvider) { m_propertyCategoryProvider = propertyCategoryProvider; } /** * @return the {@link PropertyCategory} that is used by this {@link PropertyTable} to display. */ private PropertyCategory getCategory(Property property) { return m_propertyCategoryProvider.getCategory(property); } //////////////////////////////////////////////////////////////////////////// // // PropertyInfo // //////////////////////////////////////////////////////////////////////////// /** * Class with information about single {@link Property}. * * @author scheglov_ke */ private final class PropertyInfo { private final String m_id; private final int m_level; private final Property m_property; private final boolean m_stateComplex; private boolean m_stateExpanded; private List m_children; //////////////////////////////////////////////////////////////////////////// // // Constructor // //////////////////////////////////////////////////////////////////////////// public PropertyInfo(Property property) { this(property, "", 0); } private PropertyInfo(Property property, String idPrefix, int level) { // BEGIN ADT MODIFICATIONS //m_id = idPrefix + "|" + property.getTitle(); m_id = idPrefix + "|" + property.getName(); // END ADT MODIFICATIONS m_level = level; m_property = property; m_stateComplex = property.getEditor() instanceof IComplexPropertyEditor; } //////////////////////////////////////////////////////////////////////////// // // State // //////////////////////////////////////////////////////////////////////////// /** * @return true if this property is complex. */ public boolean isComplex() { return m_stateComplex; } public boolean isShowComplex() throws Exception { if (m_stateComplex) { prepareChildren(); return m_children != null && !m_children.isEmpty(); } return false; } /** * @return true if this complex property is expanded. */ public boolean isExpanded() { return m_stateExpanded; } //////////////////////////////////////////////////////////////////////////// // // Access // //////////////////////////////////////////////////////////////////////////// /** * @return the level of this property, i.e. on which level of complex property it is located. */ public int getLevel() { return m_level; } /** * @return the {@link Property}. */ public Property getProperty() { return m_property; } /** * Flips collapsed/expanded state and adds/removes sub-properties. */ public void flip() throws Exception { Assert.isTrue(m_stateComplex); if (m_stateExpanded) { collapse(); } else { expand(); } } /** * Expands this property. */ public void expand() throws Exception { Assert.isTrue(m_stateComplex); Assert.isTrue(!m_stateExpanded); // m_stateExpanded = true; m_expandedIds.add(m_id); // BEGIN ADT MODIFICATIONS if (m_collapsedIds != null) { m_collapsedIds.remove(m_id); } // END ADT MODIFICATIONS prepareChildren(); // int index = m_properties.indexOf(this); addChildren(index + 1); } /** * Collapses this property. */ public void collapse() throws Exception { Assert.isTrue(m_stateComplex); Assert.isTrue(m_stateExpanded); // m_stateExpanded = false; m_expandedIds.remove(m_id); // BEGIN ADT MODIFICATIONS if (m_collapsedIds != null) { m_collapsedIds.add(m_id); } // END ADT MODIFICATIONS prepareChildren(); // int index = m_properties.indexOf(this); removeChildren(index + 1); } //////////////////////////////////////////////////////////////////////////// // // Internal // //////////////////////////////////////////////////////////////////////////// /** * Adds children properties. * * @return the index for new properties to add. */ private int addChildren(int index) throws Exception { prepareChildren(); for (PropertyInfo child : m_children) { // skip if should not display raw Property if (!rawProperties_shouldShow(child.m_property)) { continue; } // add child m_properties.add(index++, child); // add children of current child if (child.isExpanded()) { index = child.addChildren(index); } } return index; } /** * Removes children properties. */ private void removeChildren(int index) throws Exception { prepareChildren(); for (PropertyInfo child : m_children) { // skip if should not display raw Property if (!rawProperties_shouldShow(child.m_property)) { continue; } // hide presentation { PropertyEditorPresentation presentation = child.getProperty().getEditor().getPresentation(); if (presentation != null) { presentation.hide(PropertyTable.this, child.getProperty()); } } // remove child m_properties.remove(index); // remove children of current child if (child.isExpanded()) { child.removeChildren(index); } } } /** * Prepares children {@link PropertyInfo}'s, for sub-properties. */ private void prepareChildren() throws Exception { if (m_children == null) { m_children = Lists.newArrayList(); for (Property subProperty : getSubProperties()) { PropertyInfo subPropertyInfo = createSubPropertyInfo(subProperty); m_children.add(subPropertyInfo); } } } private PropertyInfo createSubPropertyInfo(Property subProperty) { return new PropertyInfo(subProperty, m_id, m_level + 1); } private Property[] getSubProperties() throws Exception { IComplexPropertyEditor complexEditor = (IComplexPropertyEditor) m_property.getEditor(); List subProperties = Lists.newArrayList(); for (Property subProperty : complexEditor.getProperties(m_property)) { if (getCategory(subProperty).isHidden() && !subProperty.isModified()) { // skip hidden properties continue; } subProperties.add(subProperty); } return subProperties.toArray(new Property[subProperties.size()]); } //////////////////////////////////////////////////////////////////////////// // // Persistent expanding support // //////////////////////////////////////////////////////////////////////////// /** * @return true if this {@link PropertyInfo} was expanded from history. */ public boolean expandFromHistory() throws Exception { if (isComplex() && !isExpanded() && m_expandedIds.contains(m_id)) { expand(); return true; } // BEGIN ADT MODIFICATIONS if (m_collapsedIds != null && isComplex() && !isExpanded() && !m_collapsedIds.contains(m_id)) { expand(); return true; } // END ADT MODIFICATIONS return false; } } // BEGIN ADT MODIFICATIONS /** Collapse all top-level properties */ public void collapseAll() { try { m_lastExpandCollapseTime = System.currentTimeMillis(); if (m_collapsedIds != null) { m_collapsedIds.addAll(m_expandedIds); } m_expandedIds.clear(); setInput(m_rawProperties); redraw(); } catch (Throwable e) { DesignerPlugin.log(e); } } /** Expand all top-level properties */ public void expandAll() { try { m_lastExpandCollapseTime = System.currentTimeMillis(); if (m_collapsedIds != null) { m_collapsedIds.clear(); } m_expandedIds.clear(); for (PropertyInfo info : m_properties) { if (info.m_stateComplex) { m_expandedIds.add(info.m_id); } } setInput(m_rawProperties); redraw(); } catch (Throwable e) { DesignerPlugin.log(e); } } // END ADT MODIFICATIONS }