/* * Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of Business Objects nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /* * ValueEditorManager.java * Creation date: Dec 23, 2002. * By: Edward Lam */ package org.openquark.gems.client.valueentry; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.FocusTraversalPolicy; import java.awt.FontMetrics; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.BorderFactory; import javax.swing.border.Border; import org.openquark.cal.compiler.DataConstructor; import org.openquark.cal.compiler.FieldName; import org.openquark.cal.compiler.ModuleTypeInfo; import org.openquark.cal.compiler.PreludeTypeConstants; import org.openquark.cal.compiler.QualifiedName; import org.openquark.cal.compiler.RecordType; import org.openquark.cal.compiler.ScopedEntityNamingPolicy; import org.openquark.cal.compiler.TypeConsApp; import org.openquark.cal.compiler.TypeConstructor; import org.openquark.cal.compiler.TypeExpr; import org.openquark.cal.compiler.ScopedEntityNamingPolicy.UnqualifiedUnlessAmbiguous; import org.openquark.cal.compiler.TypeChecker.TypeCheckInfo; import org.openquark.cal.module.Cal.Core.CAL_Prelude; import org.openquark.cal.module.Cal.Graphics.CAL_Color; import org.openquark.cal.module.Cal.IO.CAL_File; import org.openquark.cal.module.Cal.Utilities.CAL_Range; import org.openquark.cal.module.Cal.Utilities.CAL_RelativeTime; import org.openquark.cal.module.Cal.Utilities.CAL_Time; import org.openquark.cal.services.CALWorkspace; import org.openquark.cal.services.MetaModule; import org.openquark.cal.services.Perspective; import org.openquark.cal.valuenode.ForeignValueNode; import org.openquark.cal.valuenode.LiteralValueNode; import org.openquark.cal.valuenode.ValueNode; import org.openquark.cal.valuenode.ValueNodeBuilderHelper; import org.openquark.cal.valuenode.ValueNodeCommitHelper; import org.openquark.cal.valuenode.ValueNodeProvider; import org.openquark.cal.valuenode.ValueNodeTransformer; import org.openquark.gems.client.TypeColourProvider; import org.openquark.gems.client.utilities.SmoothHighlightBorder; import org.openquark.util.TextEncodingUtilities; import org.openquark.util.UnsafeCast; /** * This class is used to help during the construction and management of ValueEditors. * @author Edward Lam */ public final class ValueEditorManager implements TypeColourProvider { /** The namespace for log messages from the value editors. */ public static final String VALUEENTRY_LOGGER_NAMESPACE = "org.openquark.gems.client.valueentry"; /** An instance of a Logger for value editor messages. */ static final Logger VALUEENTRY_LOGGER = Logger.getLogger(VALUEENTRY_LOGGER_NAMESPACE); static { VALUEENTRY_LOGGER.setLevel(Level.FINEST); } /** The maximum preferred width allowed for any type. */ private static final int MAX_PREFERRED_WIDTH = 400; /** The BuilderHelper used throughout the ValueEditors. */ private final ValueNodeBuilderHelper valueNodeBuilderHelper; /** The type colour provider to provide type colours to the value editors. */ private final TypeColourProvider typeColourProvider; /** The type check info used by the value editor manager. */ private final TypeCheckInfo typeCheckInfo; /** The ValueNodeTransformer used to convert data between ValueNodes. */ private ValueNodeTransformer valueNodeTransformer; /** The value node commit helper for this value editor manager. */ private final ValueNodeCommitHelper valueNodeCommitHelper; /** The director is consulted when a new ValueEditor is needed in order to determine which ValueEditor to use. */ private ValueEditorDirector valueEditorDirector; /** Value editor providers that can be used to lookup and create value editors. */ private final List<ValueEditorProvider<?>> valueEditorProviders = new ArrayList<ValueEditorProvider<?>>(); /** Value node provider classes that are used to init value node builder helpers. */ private final List<Class<ValueNodeProvider<?>>> valueNodeProviderClasses = new ArrayList<Class<ValueNodeProvider<?>>>(); /** * Basically allows ValueEditors to access the Info associated with a particular ValueNode. * We use a weak hash map so that value nodes no longer in use are garbage collected (along with their info). */ private final Map<ValueNode, ValueEditor.Info> valueEditorInfoMap = new WeakHashMap<ValueNode, ValueEditor.Info>(); /** The clipboard ValueNode. */ private ValueNode clipboardValueNode = null; /** The associated workspace */ private final CALWorkspace workspace; /** * Constructor for a ValueEditorManager. * @param valueNodeBuilderHelper the valueNodeBuilderHelper * @param typeColourProvider the type colour provider */ public ValueEditorManager(ValueNodeBuilderHelper valueNodeBuilderHelper, CALWorkspace workspace, TypeColourProvider typeColourProvider, TypeCheckInfo typeCheckInfo) throws ValueEntryException { if (valueNodeBuilderHelper == null || typeColourProvider == null || typeCheckInfo == null) { throw new NullPointerException(); } this.valueNodeBuilderHelper = valueNodeBuilderHelper; this.workspace = workspace; this.typeColourProvider = typeColourProvider; this.typeCheckInfo = typeCheckInfo; initProviders(); // This needs the value node transformer to be initialized.. this.valueNodeCommitHelper = new ValueNodeCommitHelper(valueNodeBuilderHelper, valueNodeTransformer); } /** * Constructor for a ValueEditorManager. This constructor can be used to overide the value editor * director dictated by the 'registry' file. This is needed so that clients of the value entry system * can explicitly define the director they wish to use. * @param valueNodeBuilderHelper the valueNodeBuilderHelper * @param typeColourProvider the type colour provider * @param valueEditorDirector the specific director that this manager should be using */ public ValueEditorManager(ValueNodeBuilderHelper valueNodeBuilderHelper, CALWorkspace workspace, TypeColourProvider typeColourProvider, TypeCheckInfo typeCheckInfo, ValueEditorDirector valueEditorDirector) throws ValueEntryException { this(valueNodeBuilderHelper, workspace, typeColourProvider, typeCheckInfo); this.valueEditorDirector = valueEditorDirector; } /** * Returns the current valueNodeCommitHelper * @return ValueNodeCommitHelper */ public ValueNodeCommitHelper getValueNodeCommitHelper() { return valueNodeCommitHelper; } /** * Returns the current valueTransformer * @return ValueNodeTransformer */ public ValueNodeTransformer getValueNodeTransformer() { return valueNodeTransformer; } /** * Returns an input stream for accessing the value type registry data. */ protected InputStream getValueTypeRegistryData() throws IOException { String registryResource = System.getProperty("org.openquark.gems.client.valueentry.defaulttyperegistry", "/valueTypeRegistry.gvr"); InputStream stream = getClass().getResourceAsStream(registryResource); if (stream == null) { throw new IOException("System property \"" + "org.openquark.gems.client.valueentry.defaulttyperegistry" + "\" refers to nonexistent resource \"" + registryResource + "\""); } return stream; } /** * Initialize the value entry and value node providers. */ private void initProviders() throws ValueEntryException { BufferedReader reader = null; try { // Alrighty, time to parse the registry text to get the handlers. // Here are the parsing rules: // "//" at the beginning of the line denotes comments which will be ignored. // Completely Empty lines will be ignored // Sections are: // "ValueNode:" followed by the list of ValueNode handler classes, then // "ValueEditor:" followed by the list of ValueEditor handler classes, then // "ValueEditorDirector:" followed by the ValueEditorDirector handler class, then // "ValueNodeTransformer:" followed by the ValueNodeTransformer handler class. String handlerListFileLoc = System.getProperty("org.openquark.gems.client.valueentry.typeregistry", null); if (handlerListFileLoc != null) { File preludeFile = new File(handlerListFileLoc); try { reader = new BufferedReader(new FileReader(preludeFile)); } catch (FileNotFoundException e) { throw new IOException("System property \"" + "org.openquark.gems.client.valueentry.typeregistry" + "\" refers to nonexistent file \"" + handlerListFileLoc + "\""); } } else { InputStream is = getValueTypeRegistryData(); reader = new BufferedReader(TextEncodingUtilities.makeUTF8Reader(is)); } String nextLine = reader.readLine(); // Ignore comments and completely empty lines. while (nextLine != null && (nextLine.equals("") || nextLine.startsWith("//"))) { nextLine = reader.readLine(); } // We expect the 'ValueNode:' label here. if ("ValueNode:".equals(nextLine)) { nextLine = reader.readLine(); } else { throw new Exception("Error in valueTypeRegistry.txt format. 'ValueNode:' label missing."); } // Flag to indicate if we're in the next section now. boolean nextSection = false; // While we're in the ValueNode section, we keep taking the fully qualified ValueNode class name, // and performing the set-up stuff. // Also, if we hit the 'ValueEditor:' label, then we're in the next section. while (!nextSection) { // Ignore comments and completely empty lines. while (nextLine != null && (nextLine.equals("") || nextLine.startsWith("//"))) { nextLine = reader.readLine(); } if (nextLine == null) { throw new Exception("Error in valueTypeRegistry.txt format. 'ValueEditor:' label is missing."); } else if ("ValueEditor:".equals(nextLine)) { nextSection = true; } else { registerValueNodeProvider(nextLine); } nextLine = reader.readLine(); } nextSection = false; // We have different parameters for the ValueEditor constructors. // While we're in the ValueEditor section, we keep taking the fully qualified ValueEditor class name, // and performing the set-up stuff. // Also, if we hit the 'ValueEditorDirector:' label, then we're in the next section. while (!nextSection) { // Ignore comments and completely empty lines. while (nextLine != null && (nextLine.equals("") || nextLine.startsWith("//"))) { nextLine = reader.readLine(); } if (nextLine == null) { throw new Exception("Error in valueTypeRegistry.txt format. 'ValueEditorDirector:' label is missing."); } else if ("ValueEditorDirector:".equals(nextLine)) { nextSection = true; } else { registerValueEditorProvider(nextLine); } nextLine = reader.readLine(); } // Ignore comments and completely empty lines. while (nextLine != null && (nextLine.equals("") || nextLine.startsWith("//"))) { nextLine = reader.readLine(); } if (nextLine == null || nextLine.equals("ValueNodeTransformer:")) { throw new Exception("Error in valueTypeRegistry.txt format. Expecting the ValueEditorDirector class."); } else { Class<?> handlerClass = Class.forName(nextLine); Constructor<?> constructor = handlerClass.getConstructor(new Class[] {ValueNodeBuilderHelper.class}); Object newInstance = constructor.newInstance(new Object[]{valueNodeBuilderHelper}); if (newInstance instanceof ValueEditorDirector) { this.valueEditorDirector = (ValueEditorDirector)newInstance; } else { throw new Exception("Error in valueTypeRegistry.txt format. " + nextLine + " is not an instance of ValueEditorDirector."); } nextLine = reader.readLine(); } // Ignore comments and completely empty lines. while (nextLine != null && (nextLine.equals("") || nextLine.startsWith("//"))) { nextLine = reader.readLine(); } if ("ValueNodeTransformer:".equals(nextLine)) { nextLine = reader.readLine(); } else { throw new Exception("Error in valueTypeRegistry.txt format. Expecting the 'ValueNodeTransformer:' label."); } // Ignore comments and completely empty lines. while (nextLine != null && (nextLine.equals("") || nextLine.startsWith("//"))) { nextLine = reader.readLine(); } if (nextLine == null) { throw new Exception("Error in valueTypeRegistry.txt format. Expecting the ValueNodeTransformer class."); } else { Class<?> handlerClass = Class.forName(nextLine); Object newInstance = handlerClass.newInstance(); if (newInstance instanceof ValueNodeTransformer) { this.valueNodeTransformer = (ValueNodeTransformer)newInstance; } else { throw new Exception("Error in valueTypeRegistry.txt format. " + nextLine + " is not an instance of ValueNodeTransformer."); } nextLine = reader.readLine(); } // Ignore comments and completely empty lines. while (nextLine != null) { if (!nextLine.equals("") || !nextLine.startsWith("//")) { throw new Exception("Nothing more expected in HandlerList.txt"); } nextLine = reader.readLine(); } } catch (Exception e) { throw new ValueEntryException("Error in ValueEditor handler initialization.", e); } finally { if (reader != null) { try { reader.close(); } catch (IOException ex) { } } } } /** * Registers a new value editor provider and adds it to the list of providers. * Does nothing if the given name is not a valid class name. * @param className the qualified class name of the editor provider */ private void registerValueEditorProvider(String className) { try { Class<?> providerClass = Class.forName(className); if (!ValueEditorProvider.class.isAssignableFrom(providerClass)) { VALUEENTRY_LOGGER.log(Level.WARNING, "Class \"" + providerClass + "\" is not a value editor provider."); return; } Constructor<? extends ValueEditorProvider<?>> providerConstructor = UnsafeCast.unsafeCast(providerClass.getConstructor(new Class[] {ValueEditorManager.class})); // safety of cast checked above. if (providerConstructor != null) { providerConstructor.setAccessible(true); ValueEditorProvider<?> editorProvider = providerConstructor.newInstance(new Object[] {this}); valueEditorProviders.add(editorProvider); } else { VALUEENTRY_LOGGER.log(Level.WARNING, "Could not find value editor provider \"" + className + "\""); } } catch (Exception e) { VALUEENTRY_LOGGER.log(Level.WARNING, "Exception encountered initializing the value editor manager: " + e); } } /** * Registers a value node provider and adds its class to the provider list. * Does nothing if the class name is not a valid class name. * @param className the qualified class name of the provider */ private void registerValueNodeProvider(String className) { try { Class<?> providerClass = Class.forName(className); if (!ValueNodeProvider.class.isAssignableFrom(providerClass)) { VALUEENTRY_LOGGER.log(Level.WARNING, "Class \"" + providerClass + "\" is not a value node provider."); return; } Constructor<? extends ValueNodeProvider<?>> providerConstructor = UnsafeCast.unsafeCast(providerClass.getConstructor(new Class[] {ValueNodeBuilderHelper.class})); // safety of cast checked above. if (providerConstructor == null) { VALUEENTRY_LOGGER.log(Level.WARNING, "Could not find value node provider \"" + className + "\""); return; } valueNodeProviderClasses.add(UnsafeCast.<Class<ValueNodeProvider<?>>>unsafeCast(providerClass)); // safety of cast checked above. } catch (Exception e) { VALUEENTRY_LOGGER.log(Level.WARNING, "Exception encountered initializing the value editor manager: " + e); } } /** * Initialize a ValueNodeBuilderHelper based on info gained from parsing the value type registry. * @param valueNodeBuilderHelper */ public void initValueNodeBuilderHelper(ValueNodeBuilderHelper valueNodeBuilderHelper) { for (final Class<ValueNodeProvider<?>> clazz : valueNodeProviderClasses) { valueNodeBuilderHelper.registerValueNodeProvider(clazz); } } /** * Get the ValueEditorProvider to handle a given value node. * * @param valueNode the value node to find a value editor provider for * @param providerSupportInfo the support info object threaded through nested invocations. * @param outputEditorsOnly whether we should only return editors that are suitable for output * @return the value editor provider for the value node, or null if there is no provider */ private ValueEditorProvider<?> getValueEditorProvider(ValueNode valueNode, ValueEditorProvider.SupportInfo providerSupportInfo, boolean outputEditorsOnly) { if (valueNode == null) { return null; } // Search in reverse order since most recently registered providers take precedence. List<ValueEditorProvider<?>> valueEditorProviders = getValueEditorProviders(); for (int i = valueEditorProviders.size() - 1; i >= 0; i--) { ValueEditorProvider<?> provider = valueEditorProviders.get(i); if (provider.canHandleValue(valueNode, providerSupportInfo)) { // Enforce that either all editors are allowed, or the editor is suitable for output if (!outputEditorsOnly || provider.usableForOutput()) { return provider; } } } return null; } /** * @return Returns a list of all the registered value editor providers */ protected List<ValueEditorProvider<?>> getValueEditorProviders() { return valueEditorProviders; } /** * Returns the ValueEditorDirector used. * @return ValueEditorDirector */ public ValueEditorDirector getValueEditorDirector() { return valueEditorDirector; } /** * Returns the ValueNodeBuilderHelper used. * @return ValueNodeBuilderHelper */ public ValueNodeBuilderHelper getValueNodeBuilderHelper() { return valueNodeBuilderHelper; } /** * Returns a ValueEditor which can handle the indicated TypeExpr (from valueNode.getTypeExpr()). * If no such ValueEditor is found, then returns null. * @param valueEditorHierarchyManager * @param valueNode * @param onlyIncludeOutputVEPs whether we should return VEPs that are not suitable for output * @return ValueEditor */ public ValueEditor getValueEditorForValueNode(ValueEditorHierarchyManager valueEditorHierarchyManager, ValueNode valueNode, boolean onlyIncludeOutputVEPs) { ValueEditorProvider<?> provider = getValueEditorProvider(valueNode, new ValueEditorProvider.SupportInfo(), onlyIncludeOutputVEPs); List<ValueEditorProvider<?>> recommendedValueEditorList = new ArrayList<ValueEditorProvider<?>>(); if (provider != null) { recommendedValueEditorList.add(provider); } return valueEditorDirector.getValueEditor(valueNode, recommendedValueEditorList, valueEditorHierarchyManager); } /** * Indicates whether the editor for this type can be launched when * viewing output (non-editable). */ public boolean usableForOutput(ValueNode valueNode) { return getValueEditorProvider(valueNode, new ValueEditorProvider.SupportInfo(), true) != null; } /** * Get the current module. * @return MetaModule the current module, or null if there is no current module. */ public MetaModule getCurrentModule() { return getPerspective().getWorkingModule(); } /** * Get the perspective used by this manager. * @return Perspective */ public Perspective getPerspective() { return valueNodeBuilderHelper.getPerspective(); } /** * @return the associated workspace */ public CALWorkspace getWorkspace() { return workspace; } /** * @return the type check info used by this manager */ public TypeCheckInfo getTypeCheckInfo() { return typeCheckInfo; } /** * Returns the resource name of the icon for a particular type. * Creation date: (03/07/01 3:33:18 PM) * @return String * @param typeExpr */ public String getTypeIconName(TypeExpr typeExpr) { TypeConsApp typeConsApp = typeExpr.rootTypeConsApp(); String iconName = "/Resources/sometype.gif"; if (typeConsApp != null) { QualifiedName typeIde = typeConsApp.getName(); // Note: The bool check should come before the enumerated check. Since bool can count as an enumerated type. if (typeIde.equals(CAL_Prelude.TypeConstructors.Boolean)) { iconName = "/Resources/bool.gif"; } else if (typeIde.equals(CAL_Prelude.TypeConstructors.Double) || typeIde.equals(CAL_Prelude.TypeConstructors.Float) || typeIde.equals(CAL_Prelude.TypeConstructors.Decimal)) { iconName = "/Resources/float.gif"; } else if (typeIde.equals(CAL_Prelude.TypeConstructors.Int) || typeIde.equals(CAL_Prelude.TypeConstructors.Integer) || typeIde.equals(CAL_Prelude.TypeConstructors.Byte) || typeIde.equals(CAL_Prelude.TypeConstructors.Short) || typeIde.equals(CAL_Prelude.TypeConstructors.Long)) { iconName = "/Resources/integer.gif"; } else if (typeIde.equals(CAL_Prelude.TypeConstructors.Char)) { iconName = "/Resources/char.gif"; } else if (typeIde.equals(CAL_RelativeTime.TypeConstructors.RelativeDate)) { iconName = "/Resources/date.gif"; } else if (typeIde.equals(CAL_RelativeTime.TypeConstructors.RelativeTime)) { iconName = "/Resources/time.gif"; } else if (typeIde.equals(CAL_RelativeTime.TypeConstructors.RelativeDateTime) || typeIde.equals(CAL_Time.TypeConstructors.Time)) { iconName = "/Resources/datetime.gif"; } else if (typeIde.equals(CAL_Color.TypeConstructors.Color)) { iconName = "/Resources/colour.gif"; } else if (typeIde.equals(CAL_File.TypeConstructors.FileName)) { iconName = "/Resources/file.gif"; } else if (typeIde.equals(CAL_Prelude.TypeConstructors.String)) { iconName = "/Resources/string.gif"; } else if (typeIde.equals(CAL_Prelude.TypeConstructors.List)) { TypeExpr elementTypeExpr = typeConsApp.getArg(0); TypeConsApp elementTypeConsApp = elementTypeExpr.rootTypeConsApp(); if (elementTypeConsApp != null) { QualifiedName elementIde = elementTypeConsApp.getName(); if (elementIde.equals(CAL_Prelude.TypeConstructors.Char)) { iconName = "/Resources/string.gif"; } else { iconName = "/Resources/list.gif"; } } else { iconName = "/Resources/list.gif"; } } else if (typeIde.equals(CAL_Prelude.TypeConstructors.Maybe)) { iconName = "/Resources/maybetype.gif"; } else if (typeIde.equals(CAL_Prelude.TypeConstructors.Either)) { iconName = "/Resources/eithertype.gif"; } else if (getPerspective().isEnumDataType(typeIde)) { iconName = "/Resources/enum.gif"; // TODO: This is painful to write! This should be refactored somehow (like having the value // nodes return the appropriate icon rather than comparing strings). } else if (typeIde.equals(CAL_Range.TypeConstructors.Range)) { iconName = "/Resources/range.gif"; } } else if (typeExpr.rootRecordType() != null) { if (typeExpr.isTupleType()) { //a tuple-record iconName = "/Resources/tuple.gif"; } else { // A record iconName = "/Resources/record.gif"; } } else { // A parametric. iconName = "/Resources/notype.gif"; } return iconName; } /** * @param typeExpr the expression to get the name for * @return the display name of the type expression */ public String getTypeName(TypeExpr typeExpr) { ScopedEntityNamingPolicy namingPolicy = new UnqualifiedUnlessAmbiguous(getCurrentModule().getTypeInfo()); return typeExpr.toString(namingPolicy); } /** * Calculates a preferred width for text fields that display the text value of the given value node. * @param valueNode the value node * @param fontMetrics the font metrics of the text field * @param isEditable whether or not the value is editable * @return the preferred width in pixels */ public int getValuePreferredWidth(ValueNode valueNode, FontMetrics fontMetrics, boolean isEditable) { if (!isEditable) { // If we're not editable, then the value can never change. // So the baseline is the exact text value. int prefWidth = fontMetrics.stringWidth(valueNode.getTextValue()) + 5; return prefWidth < MAX_PREFERRED_WIDTH ? prefWidth : MAX_PREFERRED_WIDTH; } return getTypePreferredWidth(valueNode.getTypeExpr(), fontMetrics); } /** * Calculates a preferred width for text fields that display a text value of the given type. * @param typeExpr the type expression of the value * @param fontMetrics the font metrics for the text field * @return the preferred width in pixels */ public int getTypePreferredWidth(TypeExpr typeExpr, FontMetrics fontMetrics) { String baseline = null; int fixedWidth = 0; if (typeExpr instanceof TypeConsApp) { TypeConsApp typeConsApp = typeExpr.rootTypeConsApp(); // Estimate a reasonable preferred width for the various types. QualifiedName typeConsName = typeConsApp.getName(); if (typeConsName.equals(CAL_Prelude.TypeConstructors.Double) || typeConsName.equals(CAL_Prelude.TypeConstructors.Float) || typeConsName.equals(CAL_Prelude.TypeConstructors.Byte) || typeConsName.equals(CAL_Prelude.TypeConstructors.Int) || typeConsName.equals(CAL_Prelude.TypeConstructors.Integer) || typeConsName.equals(CAL_Prelude.TypeConstructors.Decimal) || typeConsName.equals(CAL_Prelude.TypeConstructors.Long) || typeConsName.equals(CAL_Prelude.TypeConstructors.Short)) { baseline = "999.99"; } else if (typeConsName.equals(CAL_Prelude.TypeConstructors.Char)) { baseline = "W"; } else if (typeConsName.equals(CAL_Prelude.TypeConstructors.String)) { baseline = "A reasonably long string value."; } else if (typeConsName.equals(CAL_RelativeTime.TypeConstructors.RelativeDate)) { baseline = "Wednesday, September 30, 1970"; } else if (typeConsName.equals(CAL_RelativeTime.TypeConstructors.RelativeTime)) { baseline = "12:00:00 AM"; } else if (typeConsName.equals(CAL_RelativeTime.TypeConstructors.RelativeDateTime)) { baseline = "Wednesday, September 30, 1970 12:00:00 AM"; } else if (typeConsName.equals(CAL_Time.TypeConstructors.Time)) { baseline = "Wednesday, September 30, 1970 12:00:00 AM UTC"; } else if (typeConsName.equals(CAL_Color.TypeConstructors.Color)) { fixedWidth = 30; } else if (typeConsName.equals(CAL_File.TypeConstructors.FileName)) { baseline = "/home/frank/depots/Quark/CAL"; } else if (typeConsName.equals(CAL_Prelude.TypeConstructors.Function)) { baseline = "Prelude.FunctionName"; } else if (typeConsName.equals(CAL_Prelude.TypeConstructors.List)) { TypeExpr elementTypeExpr = typeConsApp.getArg(0); TypeConsApp elementTypeConsApp = elementTypeExpr.rootTypeConsApp(); if (elementTypeConsApp != null) { QualifiedName elementTypeConsName = elementTypeConsApp.getName(); if (elementTypeConsName.equals(CAL_Prelude.TypeConstructors.Char)) { // This is just a String, so use same width. baseline = "A reasonably long string value."; } else { fixedWidth = getTypePreferredWidth(elementTypeExpr, fontMetrics); // Add space for the []. baseline = "[]"; } } else { baseline = "[] "; } } else { // This must be an algebraic or enumerated type. Use the width required for the longest // data constructor name as the preferred width. baseline = "()"; DataConstructor[] visibleDataConstructors = getPerspective().getDataConstructorsForType(typeConsName); ScopedEntityNamingPolicy namingPolicy = new UnqualifiedUnlessAmbiguous(getPerspective().getWorkingModuleTypeInfo()); if (visibleDataConstructors == null || visibleDataConstructors.length == 0) { // Include non-visible constructors if there are no visible ones. ModuleTypeInfo moduleTypeInfo = getPerspective().getMetaModule(typeConsName.getModuleName()).getTypeInfo(); TypeConstructor typeCons = moduleTypeInfo.getTypeConstructor(typeConsName.getUnqualifiedName()); for (int n = 0, count = typeCons.getNDataConstructors(); n < count; n++) { DataConstructor dataCons = typeCons.getNthDataConstructor(n); int nameLength = fontMetrics.stringWidth(dataCons.getAdaptedName(namingPolicy)); if (nameLength > fixedWidth) { fixedWidth = nameLength; } } } else { // Only include visible constructors, since the user can never switch to a non-visible // constructor which may have a larger preferred width. for (int i = 0; visibleDataConstructors != null && i < visibleDataConstructors.length; i++) { DataConstructor dataCons = visibleDataConstructors[i]; int nameLength = fontMetrics.stringWidth(dataCons.getAdaptedName(namingPolicy)); if (nameLength > fixedWidth) { fixedWidth = nameLength; } } } } } else if (typeExpr instanceof RecordType) { //todoBI preferred width is not handled //should probably do this for records in general // } else if (TypeExpr.getTupleDimension(typeConsName) >= 2) { // // for (int i = 0, nArgs = typeConstructor.getNArgs(); i < nArgs; ++i) { // TypeExpr innerTypeExpr = typeConstructor.getArg(i); // fixedWidth += getTypePreferredWidth(innerTypeExpr, fontMetrics); // } // // // Add space for the (). // baseline = "()"; } // Catch all for parametric types and anything else that is somehow not handled. if (baseline == null) { baseline = (fixedWidth != 0) ? "" : "? "; } // Add 5 pixels for some extra space (if the text field fits exactly it looks weird). int preferredWidth = 5 + fixedWidth + fontMetrics.stringWidth(baseline); // Enforce a sane maximum size. if (preferredWidth > MAX_PREFERRED_WIDTH) { preferredWidth = MAX_PREFERRED_WIDTH; } return preferredWidth; } /** * {@inheritDoc} * This simply defers to the type colour provider held by this manager. */ public Color getTypeColour(TypeExpr typeExpr) { return typeColourProvider.getTypeColour(typeExpr); } /** * Creates a focus cycle where one ValueEditor in listValueEditor will * tab to another ValueEditor in listValueEditor (the focus shift will be circular). * @param valueEditorList */ public static void createFocusCycle(final List<? extends ValueEditor> valueEditorList) { FocusTraversalPolicy focusTraversalPolicy = new FocusTraversalPolicy () { @Override public Component getComponentAfter( Container focusCycleRoot, Component aComponent) { int index = valueEditorList.indexOf(aComponent); return valueEditorList.get((index + 1) % valueEditorList.size()); } @Override public Component getComponentBefore( Container focusCycleRoot, Component aComponent) { int index = valueEditorList.indexOf(aComponent); return valueEditorList.get((index - 1) % valueEditorList.size()); } @Override public Component getDefaultComponent(Container focusCycleRoot) { return null; } @Override public Component getFirstComponent(Container focusCycleRoot) { return null; } @Override public Component getLastComponent(Container focusCycleRoot) { return null; } }; for (int i = 0, listSize = valueEditorList.size(); i < listSize; i++) { // If the editor is a value entry panel then include it in the focus cycle. If it // is some other value editor then it won't be included. Object editor = valueEditorList.get(i); if (editor instanceof ValueEntryPanel) { ((ValueEntryPanel)editor).getValueField().setFocusTraversalPolicy(focusTraversalPolicy); } } } /** * Makes a copy of the ValueNode to the manager's clipboard. * @param valueNode the value to copy to the clipboard */ public void copyToClipboard(ValueNode valueNode) { clipboardValueNode = valueNodeBuilderHelper.buildValueNode(valueNode.getValue(), null, valueNode.getTypeExpr()); } /** * Get (a copy of..) the clipboard value if any. * @return ValueNode the clipboard value node, or null if none. */ public ValueNode getClipboardValue() { if (clipboardValueNode == null) { return null; } TypeExpr newTypeExpr = clipboardValueNode.getTypeExpr().copyTypeExpr(); // return a copy return valueNodeBuilderHelper.buildValueNode(clipboardValueNode.getValue(), null, newTypeExpr); } /** * Associate a valueNode with some info. * @param valueNode * @param info * @return ValueEditor.Info any previously associated info. */ public ValueEditor.Info associateInfo(ValueNode valueNode, ValueEditor.Info info) { return valueEditorInfoMap.put(valueNode, info); } /** * Get the info (if any) associated with a value node. * @param valueNode * @return ValueEditor.Info */ public ValueEditor.Info getInfo(ValueNode valueNode) { return valueEditorInfoMap.get(valueNode); } /** * Finds all types available for input. * Available for input means that the type is handleable by a value editor and also that * the type is visible from the current module. * @return a set of all types available for input. */ public Set<TypeExpr> getAvailableInputTypes() { TypeConstructor[] entities = getPerspective().getTypeConstructors(); Set<TypeExpr> types = new HashSet<TypeExpr>(); ValueEditorProvider.SupportInfo supportInfo = new ValueEditorProvider.SupportInfo(); for (final TypeConstructor typeCons : entities) { TypeConsApp typeConsApp = getValueNodeBuilderHelper().getTypeConstructorForEntity(typeCons); if (typeConsApp != null) { ValueNode valueNode = valueNodeBuilderHelper.getValueNodeForTypeExpr(typeConsApp); if (getValueEditorProvider(valueNode, supportInfo, false) != null) { types.add(typeConsApp); } } } // The Record type is special since it has no type constructor entity, but // there is a value node / value editor provider to handle it. Set<FieldName> fieldNames = new HashSet<FieldName>(); TypeExpr recordType = TypeExpr.makeFreeRecordType(fieldNames); ValueNode valueNode = valueNodeBuilderHelper.getValueNodeForTypeExpr(recordType); if (getValueEditorProvider(valueNode, supportInfo, false) != null) { types.add(recordType); } // Prelude.Char is special since it can be directly entered into a VEP, but // there is no actual value editor provider for the Char type. types.add(valueNodeBuilderHelper.getPreludeTypeConstants().getCharType()); return types; } /** * Check if a default value of the given type is available to be input by a value editor. * This checks for default editor providability and input type editability. * @param typeToCheck the type to check availability for * @return true if a default value for the type can be input by a value editor, false otherwise */ public boolean canInputDefaultValue(TypeExpr typeToCheck) { if (canEditInputType(typeToCheck)) { return !(valueNodeBuilderHelper.getValueNodeForTypeExpr(typeToCheck) instanceof ForeignValueNode); } return false; } /** * Check if the given type can be edited by a value editor. * This means that the type is handleable by a value editor and also that the type is visible from the current module. * @param typeToCheck the type to check availability for * @return true if the type can be input by a value editor, false otherwise */ public boolean canEditInputType(TypeExpr typeToCheck) { return canEditInputType(typeToCheck, new ValueEditorProvider.SupportInfo()); } /** * Check if the given type can be edited by a value editor. * This means that the type is handleable by a value editor and also that the type is visible from the current module. * @param typeToCheck the type to check availability for * @param supportInfo the support info object threaded through nested invocations. * @return true if the type can be input by a value editor, false otherwise */ public boolean canEditInputType(TypeExpr typeToCheck, ValueEditorProvider.SupportInfo supportInfo) { if (typeToCheck == null) { return false; } else if (typeToCheck.sameType(valueNodeBuilderHelper.getPreludeTypeConstants().getCharType())) { // Char type is special, since it can be directly entered into a VEP, // but does not have an actual value editor. return true; } else if (supportInfo.isSupported(typeToCheck)) { return true; } else { // Return whether there is a value editor for default value node of the given type. ValueNode valueNode = valueNodeBuilderHelper.getValueNodeForTypeExpr(typeToCheck); return valueNode != null && getValueEditorProvider(valueNode, supportInfo, false) != null; } } /** * Checks if the given value node is supported by a value editor. * @param valueNode the value node to check * @return true if the value node is supported by a value editor */ public boolean isSupportedValueNode(ValueNode valueNode, ValueEditorProvider.SupportInfo supportInfo) { if (supportInfo.isSupported(valueNode.getTypeExpr())) { return true; } if (valueNode instanceof LiteralValueNode && valueNode.getTypeExpr().sameType(valueNodeBuilderHelper.getPreludeTypeConstants().getCharType())) { // Char type is special, since it can be directly entered into a VEP, // but does not have an actual value editor. return true; } else { return getValueEditorProvider(valueNode, supportInfo, false) != null; } } /** * Clamps the actual value between the min and max value. * @param min the minimum allowed value * @param actual the actual value * @param max the maximum allowed value * @return If actual falls within range, then actual is returned. * If actual > max, then max is returned. * If actual < min, then min is returned. */ public static int clamp(int min, int actual, int max) { return actual < min ? min : actual > max ? max : actual; } /** * Using the type colour and moveable flag this method will construct a border object suitable for * a value editor object. The parameters may be ignored by some implementations * @param valueEditor The value editor that we are creating a border for. Must be non-null * and should be initialized before making this call. * @return The base value editor manager will return a default etched border for value entry panels * or a smooth highlight border that is customized to use the value editor's background colour and * moveable state for all other editors. Note that subclasses can return null to indicate there * should be no border. */ public Border getValueEditorBorder(ValueEditor valueEditor) { // Value entry panels and pick list value editors always use an etched border if (valueEditor instanceof ValueEntryPanel || valueEditor instanceof PickListValueEditor) { return BorderFactory.createEtchedBorder(); } else { return new SmoothHighlightBorder(valueEditor.getBackground(), valueEditor.isMoveable()); } } /** * Returns true if the value entry panels should use borders around the type icon and * field components. Returns false if no borders should be used. * @return The base value editor manager always returns true. */ public boolean useValueEntryPanelBorders() { return true; } /** * Returns true if the value entry panels should show their type icon and false if it should * be hidden. * @return The base value editor manager always returns true. */ public boolean showValueEntryPanelTypeIcon() { return true; } /** * Returns true if the type colour hinting should be used. In particular, this applies to * value editors which may choose to change their background colour and internal borders. * @return The base value editor manager always returns true. */ public boolean useTypeColour() { return true; } /** * @return handy TypeExpr constants for common Prelude types. */ public PreludeTypeConstants getPreludeTypeConstants() { return valueNodeBuilderHelper.getPreludeTypeConstants(); } /** * @param typeExpr the type expression to check * @return whether values of the type could be edited directly by editing the text in a text field. */ public boolean isTextEditable(TypeExpr typeExpr) { PreludeTypeConstants typeConstants = getPreludeTypeConstants(); return typeExpr.sameType(typeConstants.getCharType()) || typeExpr.sameType(typeConstants.getByteType()) || typeExpr.sameType(typeConstants.getShortType()) || typeExpr.sameType(typeConstants.getIntType()) || typeExpr.sameType(typeConstants.getIntegerType()) || typeExpr.sameType(typeConstants.getDecimalType()) || typeExpr.sameType(typeConstants.getLongType()) || typeExpr.sameType(typeConstants.getDoubleType()) || typeExpr.sameType(typeConstants.getStringType()) || typeExpr.sameType(typeConstants.getCharListType()); } /** * @param typeExpr the type expression to check * @return whether values of the type could be edited directly from a text field. This is more * general than text editability, since it could just mean the up/down arrow keys can change the * value, as opposed to freeform typing of a value. */ public boolean isFieldEditable(TypeExpr typeExpr) { TypeConsApp typeConsApp = typeExpr.rootTypeConsApp(); return isTextEditable(typeExpr) || typeExpr.isNonParametricType(CAL_RelativeTime.TypeConstructors.RelativeDate) || typeExpr.isNonParametricType(CAL_RelativeTime.TypeConstructors.RelativeTime) || typeExpr.isNonParametricType(CAL_RelativeTime.TypeConstructors.RelativeDateTime) || typeConsApp != null && getPerspective().isEnumDataType(typeConsApp.getName()); } }