From 765e52e2d30d0754625b8c7af6c36e93612f15be Mon Sep 17 00:00:00 2001 From: Tor Norbye Date: Wed, 4 Apr 2012 18:36:45 -0700 Subject: Add WindowBuilder propertysheet code. See README.txt for details. This reverts commit b33fa4c8ce6b3d2c10eaa92c5c26019d7326bd4c. Change-Id: I763357c42f933a52eda9b52988b785e092eac424 --- .../core/model/property/table/PropertyTable.java | 1602 ++++++++++++++++++++ 1 file changed, 1602 insertions(+) create mode 100644 propertysheet/src/org/eclipse/wb/internal/core/model/property/table/PropertyTable.java (limited to 'propertysheet/src/org/eclipse/wb/internal/core/model/property/table/PropertyTable.java') diff --git a/propertysheet/src/org/eclipse/wb/internal/core/model/property/table/PropertyTable.java b/propertysheet/src/org/eclipse/wb/internal/core/model/property/table/PropertyTable.java new file mode 100644 index 0000000..7a49cb3 --- /dev/null +++ b/propertysheet/src/org/eclipse/wb/internal/core/model/property/table/PropertyTable.java @@ -0,0 +1,1602 @@ +/******************************************************************************* + * 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.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.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 (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 +} \ No newline at end of file -- cgit v1.2.3