/*
* Copyright 2003-2010 Tufts University Licensed under the
* Educational Community License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may
* obtain a copy of the License at
*
* http://www.osedu.org/licenses/ECL-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an "AS IS"
* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package tufts.vue;
import static tufts.vue.LWComponent.*;
import tufts.Util;
import java.beans.*;
import java.util.*;
import javax.swing.JLabel;
public class EditorManager
implements LWSelection.Listener,
LWComponent.Listener,
PropertyChangeListener
{
private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(EditorManager.class);
private static final Collection<LWEditor> mEditors = new HashSet<LWEditor>();
private static final Map<LWEditor,JLabel> mLabels = new HashMap();
private static EditorManager singleton;
private static boolean EditorLoadingUnderway; // editors are loading values from the selection
private static boolean PropertySettingUnderway; // editor values are being applied to the selection
/** Property bits for any editor state changes in the "free" state (nothing is
* selected, all editors are enabled -- property changes are not being
* applied directly to any LWComponents in a selection).
*/
private static long FreePropertyBits;
// now that we're tracking these, we may be able to do away with the provisionals:
// resolution becomes asking the editors to produce the value for the recorded
// free bits, and applying those to the typed style (ignoring/tossing free bits
// not supported on the target style).
private static final long PERMITTED_TEXT_BITS = ~ (LWKey.FillColor.bit | LWKey.Shape.bit);
private static class StyleType {
final Object token;
final LWComponent style;
final LWComponent provisional;
StyleType(Object t, LWComponent s, LWComponent p) {
token = t;
style = s;
provisional = p;
}
void resolveToProvisional(long freeBits) {
if (DEBUG.STYLE && (DEBUG.META /*|| DEBUG.WORK*/)) tufts.Util.printStackTrace(this + " RESOLVING TO " + provisional);
if (token == LWNode.TYPE_TEXT || token == LWText.TYPE_RICHTEXT) {
// special case for "text" nodes:
// don't assume shape or fill was pre-ordained for the text object
// -- these can only be set after the object is created. (And as it
// stands at the moment changing the fill color to will actually
// change it's type from "textNode" back to LWNode.class).
if (freeBits != 0) {
freeBits &= PERMITTED_TEXT_BITS;
style.copyProperties(provisional, freeBits);
} else {
style.copyStyle(provisional, PERMITTED_TEXT_BITS);
}
} else {
if (freeBits != 0)
style.copyProperties(provisional, freeBits);
else
style.copyStyle(provisional);
}
}
/** @return the style ready for use to be applied to something -- does sanity checking
* on the style before returning it.
*/
LWComponent produceStyle()
{
if (token == LWNode.TYPE_TEXT || token == LWText.TYPE_RICHTEXT) {
// special case for "text" nodes:
Object shape;
if (style.isTransparent() &&
style.getStrokeWidth() <= 0 &&
style.supportsProperty(LWKey.Shape) &&
style.getPropertyValue(LWKey.Shape) != java.awt.geom.Rectangle2D.Float.class)
{
// If completely transparent, assume no point in having a shape that's non-rectangular
style.setProperty(LWKey.Shape, java.awt.geom.Rectangle2D.Float.class);
// just in case, make sure synced with provisional:
provisional.setProperty(LWKey.Shape, java.awt.geom.Rectangle2D.Float.class);
}
}
return style;
}
void takeProperty(String source, Object key, Object newValue) {
// if current token is LWNode.class, and we're going transparent,
// that will switch us to "textNode", so we may not want to update
// in that case -- no easy way around this if you can convert back
// and forth between textNode and regular node by switching the fill color
applyPropertyValue("<" + source + ":typeSync>", key, newValue, style);
applyPropertyValue("<" + source + ":provSync>", key, newValue, provisional);
}
void discardProvisional() {
provisional.copyStyle(style);
}
public String toString() {
return style.toString();
}
}
private static StyleType CurrentStyle;
private static StyleType CurrentToolStyle;
private static final Map<Object,StyleType> StylesByType = new HashMap();
private LWComponent singleSelection;
/**
* Only LWEditors created and put in the AWT hierarchy before this
* is called will be recognized.
*/
public static void install() {
// make sure it runs in AWT, as will be scanning AWT heirarchy, and may be
// interrogating java.awt.Components
Log.debug("install...");
tufts.vue.gui.GUI.invokeOnEDT(new Runnable() { public void run() { _install(); }});
}
/*
Used by the applet on mac which has to be a lot more literal
about destroying objects when recycling itself then it does on windows
*/
public static void destroy()
{
//mEditors.clear();
//mLabels.clear();
singleton = null;
}
private static synchronized void _install() {
if (singleton != null) {
tufts.Util.printStackTrace("can only have one instance of " + singleton);
} else {
Log.debug("INSTALLING...");
singleton = new EditorManager();
preLoadStyle(tufts.vue.NodeTool.NodeModeTool.createDefaultNode("editor-node-style"));
preLoadStyle(tufts.vue.NodeTool.NodeModeTool.createDefaultTextNode("editor-text-style"));
//preLoadStyle(new LWLink(null, null));
singleton.refresh();
}
}
/** find all LWEditors in the AWT hierarchy and register them */
public static synchronized void refresh() {
// make sure it runs on the AWT EDT, as this will be scanning the AWT heirarchy
tufts.vue.gui.GUI.invokeOnEDT(new Runnable() { public void run() {
singleton.findEditors();
}});
}
private EditorManager() {
VUE.getSelection().addListener(this);
VUE.addActiveListener(LWMap.class, this);
VUE.addActiveListener(VueTool.class, this);
}
public void selectionChanged(LWSelection s) {
if (DEBUG.STYLE) out("selectionChanged: " + s);
// todo: if selection goes from 1 to 0, and we have an active
// tool, we might want to revert the editor states to the
// provisional for the active tool, so you can again see
// the props for what you'd be about to create.
if (s.size() == 1) {
if (singleSelection != null)
singleSelection.removeLWCListener(this);
singleSelection = s.first();
// // This seems a bit agressive:
// if (FreePropertyBits != 0) {
// // if we select something with unapplied changes (free properties),
// // apply them to the selection. We should be moving from a nothing
// // selected to a new selection state, as we should only ever have
// // unapplied changes if there wasn't a selection to apply them to.
// resolveToProvisionalStyle(singleSelection, FreePropertyBits);
// applyCurrentProperties(singleSelection, FreePropertyBits);
// }
// add the listener after auto-applying any free style info
singleSelection.addLWCListener(this);
CurrentStyle = getStyleForType(singleSelection);
} else {
// TODO: it will be easy for the selection to keep a hash of contents based
// on typeToken, so we can at least know in multi-selection cases if
// they're all of the same type, and update the right style holder,
// as opposed to requiring a single selection to update it.
//CurrentStyleType = null;
if (singleSelection != null) {
singleSelection.removeLWCListener(this);
singleSelection = null;
}
}
loadAllEditors(s);
}
public void activeChanged(ActiveEvent e, VueTool tool)
{
//out("ACTIVE TOOL temp=" + tool.isTemporary() + " " + tool);
if (tool == null || VUE.getSelection().size() > 0 || tool.isTemporary()) {
if (tool == null)
CurrentToolStyle = null;
return;
}
CurrentToolStyle = null;
final Object typeToken = tool.getSelectionType();
if (typeToken == null)
return;
if (FreePropertyBits != 0) {
// if we switch to a new tool, and there were editor
// changes that had been unused, target them for the new
// tool type if there is one.
resolveToProvisionalStyle(typeToken, FreePropertyBits);
}
final StyleType oldStyle = CurrentStyle;
CurrentStyle = CurrentToolStyle = StylesByType.get(typeToken);
if (CurrentStyle != null && CurrentStyle != oldStyle)
loadAllEditorValues(CurrentStyle.style);
}
private boolean extractedDefaultTypesFromMap = false;
public void activeChanged(ActiveEvent e, LWMap map)
{
if (map != null && map.hasChildren() && !extractedDefaultTypesFromMap) {
// performance: might be alot to do every time we switch the active map...
extractedDefaultTypesFromMap = true;
extractMostRecentlyUsedStyles(map);
// Update the active style types and tool states with any newly
// extracted default styles:
if (CurrentStyle != null)
CurrentStyle = StylesByType.get(CurrentStyle.token);
if (CurrentToolStyle != null)
CurrentToolStyle = StylesByType.get(CurrentToolStyle.token);
if (CurrentStyle != null)
loadAllEditorValues(CurrentStyle.style);
}
}
/** If the single object in the selection has a property change that was NOT due to an editor,
* (e.g., a menu) we detect this here, and re-load the editors as needed.
*/
public void LWCChanged(LWCEvent e) {
if (EditorLoadingUnderway || PropertySettingUnderway)
; // ignore
// else if (e.getKey() != null && e.getKey().type == LWComponent.KeyType.STYLE) {
// // above assumes LWEditors only handle style types...
else if (e.getKey() != null) { // only listen for real Key's
loadAllEditors(VUE.getSelection());
}
// TODO performance:
// really, we only need to load the one editor for the key in LWCEvent
// Doing this ways will constantly load all the editors, even tho
// they don't need it. (We could make a hash of all the keys
// the editors listen for, and check that here).
}
public void propertyChange(PropertyChangeEvent e) {
if (!EditorLoadingUnderway && e instanceof LWPropertyChangeEvent) {
if (DEBUG.TOOL) out("propertyChange: " + e);
try {
applySinglePropertyChange(((LWPropertyChangeEvent)e).key, e.getNewValue(), e.getSource());
} catch (Throwable t) {
tufts.Util.printStackTrace(t, this + ": failed to handle property change event: " + e);
}
}
}
private void loadAllEditorValues(LWComponent style) {
loadAllEditors(new LWSelection(style), style, false);
}
private void loadAllEditors(LWSelection selection) {
final LWComponent editorStateSource;
if (selection.size() == 1)
editorStateSource = selection.first();
// i think the below case is conflicting with our CurrentStyle code
//else if (selection.getStyleRecord() != null)
//editorStateSource = selection.getStyleRecord();
else
editorStateSource = null;
loadAllEditors(selection, editorStateSource, true);
}
// TODO: can get rid of passing in selection: only need editable property bits
private void loadAllEditors(LWSelection selection, LWComponent propertyValueSource, boolean setEnabledStates)
{
if (DEBUG.TOOL||DEBUG.STYLE) {
String msg = "loadAllEditors from: " + propertyValueSource
+ "; currentTypedStyle: " + CurrentStyle
+ " updateEnabled=" + setEnabledStates
+ " " + selection;
if (DEBUG.META/*||DEBUG.WORK*/)
tufts.Util.printStackTrace(msg);
else
out(msg);
}
// While the editors are loading, we want to ignore any change events that
// loading may produce in the editors (otherwise, we'd then set the selected
// component properties, end end up risking recursion, even tho properties
// shouldn't be triggering events if their value hasn't actually changed)
EditorLoadingUnderway = true;
try {
for (LWEditor editor : mEditors) {
try {
setEditorState(editor, selection, propertyValueSource, setEnabledStates);
} catch (Throwable t) {
tufts.Util.printStackTrace(t, this + ": general failure processing LWEditor: " + editor);
}
}
} finally {
EditorLoadingUnderway = false;
}
}
private static String dumpEditor(LWEditor editor) {
String msg = tufts.vue.gui.GUI.name(editor);
if (DEBUG.TOOL) {
msg += ";\n\tObject: " + editor;
if (editor instanceof java.awt.Component) {
java.awt.Component parent = ((java.awt.Component)editor).getParent();
msg += "\n\tAWT parent: " + tufts.vue.gui.GUI.name(parent) + "; " + parent;
}
}
return msg;
}
private void setEditorState(LWEditor editor, LWSelection selection, LWComponent propertySource, boolean setEnabledState) {
final boolean supported;
if (selection.isEmpty()) {
supported = true;
} else {
supported = selection.hasEditableProperty(editor.getPropertyKey());
//supported = (supportedPropertyBits & editor.getPropertyKey().bit) != 0;
}
if (DEBUG.TOOL) out("SET-ENABLED " + (supported?"YES":" NO") + ": " + editor);
if (setEnabledState) {
try {
editor.setEnabled(supported);
} catch (Throwable t) {
tufts.Util.printStackTrace(t, this + ": LWEditor.setEnabled failed on: " + editor);
}
if (mLabels.containsKey(editor))
mLabels.get(editor).setEnabled(supported);
}
final Object key = editor.getPropertyKey();
if (key == null) {
//Log.debug("editor reports null property key: " + dumpEditor(editor));
return;
}
//final Object value = propertySource.getPropertyValue(key);
//if (DEBUG.TOOL&&DEBUG.META) out("loadEditor: " + editor + " loading " + editor.getPropertyKey() + " from " + source);
//if (supported && propertySource != null && propertySource.supportsProperty(editor.getPropertyKey()))
if (supported && propertySource != null)
loadEditorWithValue(editor, propertySource.getPropertyValue(key));
// TODO: a bit overkill to do this no matter what -- do we need to if no property source?
if (propertySource != null)
recordPropertyChangeInStyles("load", editor.getPropertyKey(), editor.produceValue(), selection.isEmpty());
//if (editor instanceof Component) ((Component)editor).repaint(); // not helping ShapeIcon's repaint when disabled...
}
private void loadEditorWithValue(LWEditor editor, Object value) {
if (DEBUG.TOOL) out(" loadEditor: " + editor + " <- value[" + value + "]");
try {
editor.displayValue(value);
} catch (Throwable t) {
tufts.Util.printStackTrace(t, this + ": LWEditor.displayValue failed on: " + editor + " with value [" + value + "]");
}
//} else if (DEBUG.TOOL) out("\tloadEditor: " + source + " -> " + editor + " skipped; null value for " + key);
}
public static void firePropertyChange(LWEditor editor, Object source) {
try {
applySinglePropertyChange(editor.getPropertyKey(), editor.produceValue(), source);
} catch (Throwable t) {
Log.error("failed to fire property for LWEditor " + editor + " for source " + source, t);
}
}
public static boolean isEditorLoading()
{
return EditorLoadingUnderway;
}
public static void setEditorLoading(boolean loading)
{
EditorLoadingUnderway = loading;
}
/** Will either modifiy the active selection, or if it's empty, modify the default state (creation state) for this tool panel */
private static void applySinglePropertyChange(final Object key,
final Object newValue,
Object source)
{
final LWSelection selection = VUE.getSelection();
final Collection<LWComponent> components = selection;
if (EditorLoadingUnderway) {
if (DEBUG.TOOL) out("applySinglePropertyChange: " + key + " " + newValue + " (skipping)");
return;
}
if (DEBUG.TOOL||DEBUG.STYLE) out("applySinglePropertyChange: " + key + " " + newValue);
if (!components.isEmpty()) {
consumeFreeProperties();
// As setting these properties in the model will trigger notify events from the selected objects
// back up to the tools, we want to ignore those events while this is underway -- the tools
// already have their state set to this.
PropertySettingUnderway = true;
try {
for (tufts.vue.LWComponent c : components)
applyPropertyValue(source, key, newValue, c);
} finally {
PropertySettingUnderway = false;
}
if (selection.getStyleRecord() != null)
applyPropertyValue(source, key, newValue, selection.getStyleRecord());
if (VUE.getUndoManager() != null)
VUE.getUndoManager().markChangesAsUndo(key.toString());
recordPropertyChangeInStyles("apply", key, newValue, false);
} else {
recordPropertyChangeInStyles("apply", key, newValue, true);
}
}
private static void declareFreeProperty(Object k) {
if (k instanceof Key) {
final Key key = (Key) k;
// Note that if any LWEditors are created that are handling non KeyType.STYLE properties,
// we could get some odd effects (e.g., we wouldn't want label or notes being
// tagged as having been free property bit, then applying it to newly created objects!)
if (key.type != KeyType.STYLE && key.type != KeyType.SUB_STYLE) {
if (DEBUG.Enabled) Log.debug("note: free property of non-style type being ignored: " + key + "; type=" + key.type);
// we can safely ignore this, but debug for now so we know if we get into this situation
return;
}
FreePropertyBits |= key.bit;
if (DEBUG.STYLE) out("declaring free (unused) property change: " + key + "; type=" + key.type);
//Util.printStackTrace("HERE");
}
}
private static void consumeFreeProperties() {
if (DEBUG.STYLE) out("consuming free property bits: " + Long.bitCount(FreePropertyBits));
FreePropertyBits = 0;
}
private static void recordPropertyChangeInStyles
(String debugSrc,
Object key,
Object newValue,
boolean provisionals)
{
// provisionals should be true when there is no selection, and the tools are all enabled in their "free" state
if (provisionals) {
declareFreeProperty(key);
for (StyleType styleType : StylesByType.values()) {
applyPropertyValue("<" + debugSrc + ":provSync>", key, newValue, styleType.provisional);
if (CurrentToolStyle == styleType)
applyPropertyValue("<" + debugSrc + ":provSync>", key, newValue, styleType.style);
}
} else if (CurrentStyle == null) {
if (DEBUG.STYLE) out("NO CURRENT STYLE FOR " + debugSrc + " " + key + " " + newValue);
} else {
CurrentStyle.takeProperty(debugSrc, key, newValue);
}
}
/**
* Apply the current appropriate properties to the given newly created object based on it's type.
*/
public static void applyCurrentProperties(LWComponent c) {
applyCurrentProperties(c, 0L);
}
private static void applyCurrentProperties(LWComponent c, long freeBits) {
if (c == null)
return;
try {
// TODO: we can fix the problem of all font properties coming along if you
// change any one of them (because copyStyle currently copies the entire
// font property at once -- it doesn't do it by sub-property) by dumping the
// provisionals code -- we just have to extract any free properties from the
// LWEditors whenever we do this, and apply them to type typed style,
// creating a temporary style to use without having to resolve it to a
// target (a transient resolve). E.g.: the creation node / creation link:
// we want to use the current properties for drawing the newly drag-created
// object, but we don't want to target them until / unless they actually
// create the node/link, which can be aborted during the drag operation.
// Currently tho, this is actually NOT a problem, as neither the creation
// node or link draw text and (using the font) while being dragged,
// so unless that happens, we won't be hit by the font limitation.
StyleType styleType = StylesByType.get(typeToken(c));
if (styleType != null) {
if (freeBits != 0)
c.copyProperties(styleType.provisional, freeBits);
else
c.copyStyle(styleType.provisional);
}
} catch (Throwable t) {
tufts.Util.printStackTrace(t, "failed to apply current properties to: " + c);
}
}
/**
* Apply the current appropriate properties to the given newly created object based
* on it's type. If nothing was selected, and the the editors were all enabled in
* their "free" state (not tied to the property of something selected) resolve that
* any changes to their free state were meant for the type of the given node.
*/
public static void targetAndApplyCurrentProperties(LWComponent c) {
try {
LWComponent styleForType = resolveToProvisionalStyle(c);
consumeFreeProperties();
if (styleForType != null)
c.copyStyle(styleForType);
} catch (Throwable t) {
tufts.Util.printStackTrace(t, "failed to target and apply current properties to: " + c);
}
}
private static void applyPropertyValue(Object debugSrc, Object key, Object newValue, LWComponent target) {
//if (DEBUG.STYLE) System.out.println("APPLY " + debugSrc + " " + key + "[" + newValue + "] -> " + target);
if (target.supportsProperty(key)) {
if (DEBUG.STYLE) out(String.format("APPLY %s %-15s %-40s -> %s",
debugSrc,
key,
"(" + newValue + ")",
target));
//Util.tags(target)));
try {
target.setProperty(key, newValue);
//} catch (LWComponent.PropertyValueVeto ex) {
//tufts.Util.printStackTrace(ex);
} catch (Throwable t) {
tufts.Util.printStackTrace(t, debugSrc + " failed to set property " + key + "; value=" + newValue + " on " + target);
}
}
}
private synchronized StyleType getStyleForType(LWComponent c) {
final Object token = typeToken(c);
StyleType styleType = token == null ? null : StylesByType.get(token);
if (styleType == null && token != null) {
styleType = putStyle(token, createStyle(c, token));
//styleHolder = createStyle(c, token, "style");
//StylesByType.put(token, styleHolder);
}
if (DEBUG.STYLE) out("got styleHolder for type token (" + token + "): " + Util.tags(styleType));
return styleType;
}
private static StyleType putStyle(Object token, LWComponent style) {
StyleType newType = new StyleType(token,
style, // style
createStyle(style.getClass(), style, token, "provi")); // provisional
StylesByType.put(token, newType);
return newType;
}
private static void preLoadStyle(LWComponent c) {
putStyle(c.getTypeToken(), c);
}
// private static void preLoadStyle(Class<? extends LWComponent> clazz) {
// preLoadStyle(clazz, clazz);
// }
// private static void preLoadStyle(Class<? extends LWComponent> clazz, Object typeToken) {
// final LWComponent preLoadStyle = createStyle(clazz, null, typeToken, "INIT-");
// putStyle(typeToken, preLoadStyle);
// }
private static final LWComponent.CopyContext DUPE_WITHOUT_CHILDREN = new LWComponent.CopyContext(false);
private static LWComponent createStyle(LWComponent styleSource, Object typeToken) {
//return createStyle(styleSource, typeToken, "style");
return createStyle(styleSource.getClass(), styleSource, typeToken, "style");
}
// @param version is for debug
//private static LWComponent createStyle(LWComponent styleSource, Object typeToken, String version)
private static LWComponent createStyle
(Class<? extends LWComponent> clazz,
LWComponent styleSource, // may be null if only clazz is known
Object typeToken,
String version)
{
//if (DEBUG.STYLE || DEBUG.WORK) out("creating style holder based on " + styleSource + " for type (" + typeToken + ")");
// As any LWComponent can be used as a style source,
// we can just dupe whatever we've got (a handy
// way to instance another component with the same typeToken)
//final LWComponent style = styleSource.duplicate(DUPE_WITHOUT_CHILDREN);
// Too risky: e.g., styles getting image updates? Not fatal, but messy
// / confusing to debug.
LWComponent style = null;
try {
style = clazz.newInstance();
} catch (Throwable t) {
Util.printStackTrace(t, "newInstance " + clazz);
return null;
}
if (styleSource != null) {
if (styleSource.getClass() != clazz)
Log.warn("class mis-match: " + clazz + " != " + Util.tags(styleSource), new Throwable("HERE"));
style.copySupportedProperties(styleSource);
style.copyStyle(styleSource);
}
style.setFlag(LWComponent.Flag.EVENT_SILENT);
style.setPersistIsStyle(Boolean.TRUE); // mark as a style: e.g., so if link, can know not to recompute
//-----------------------------------------------------------------------------
// for clear debugging info only:
style.enableProperty(LWKey.Label); // in case it's override getLabel like LWSlide
// note: property bits are duplicated, so if we ever change from a model of
// just copying the style from the style to using it as a base for
// duplication, we have to leave the original bits in place.
style.setLabel("<" + version + ":" + typeToken + ">");
// style.setResource((Resource)null); // clear out any resource if it had it
// style.setNotes(null);
// style.takeLocation(0,0);
// style.takeSize(100,100);
//-----------------------------------------------------------------------------
//out("created new styleHolder for type token [" + token + "]: " + styleHolder);
//out("created " + styleHolder);
if (DEBUG.STYLE) out("CREATED STYLE: type=" + typeToken + "; store=" + style + " based on " + styleSource);
return style;
}
// private static LWComponent resolveToProvisionalStyleFor(LWComponent c) {
// if (c != null)
// return resolveToProvisionalStyle(c.getTypeToken());
// else
// return null;
// }
private static LWComponent resolveToProvisionalStyle(Object typeToken) {
return resolveToProvisionalStyle(typeToken, 0L);
}
/** @return the current style for type type of the given component only if we already have one
* -- do not auto-create a new style for the type if we don't already have one */
private static LWComponent resolveToProvisionalStyle(Object typeToken, long freeBits)
{
if (DEBUG.STYLE) out("resolveToProvisionalStyle: " + typeToken);
if (typeToken == null)
return null;
if (typeToken instanceof LWComponent) {
//Util.printStackTrace("oops; passed LWComponent as type-token: " + typeToken);
typeToken = typeToken((LWComponent)typeToken);
if (DEBUG.STYLE) out("resolveToProvisionalStyle: " + typeToken);
}
if (StylesByType == null) {
tufts.Util.printStackTrace("circular static initializer dependency");
return null;
}
StyleType resolver = StylesByType.get(typeToken);
// move the provisional style (unselected tool state style) to the actual
// style for this type:
if (resolver != null) {
resolver.resolveToProvisional(freeBits);
if (DEBUG.STYLE || DEBUG.WORK) out("Resolved provisional type to final applied type: " + resolver);
consumeFreeProperties();
}
// new reset all other provisional styles: the tool state change has been
// resolved to been have meant for an object of the type just created
for (StyleType styleType : StylesByType.values())
if (styleType != resolver)
styleType.discardProvisional();
return resolver == null ? null : resolver.produceStyle();
}
// public static Object GetPropertyValue(LWComponent.Key key) {
// for (LWEditor editor : mEditors) {
// if (editor.getPropertyKey() == key)
// return editor.produceValue();
// }
// return null;
// }
/*
public static void ApplyProperties(LWComponent c) {
ApplyProperties(c, ~0L);
}
*/
/*
* Apply the current value of all selected tools that are applicable to the given component.
* E.g., used for setting the properties of newly created objects.
* @param keyBits -- only apply keys whose bit is represented in keyBits (@see LWComonent.Key.bit)
*
public static void ApplyProperties(LWComponent c, long keyBits) {
for (LWEditor editor : mEditors) {
final Object k = editor.getPropertyKey();
final LWComponent.Key key;
if (k instanceof LWComponent.Key)
key = (LWComponent.Key) k;
else {
key = null;
out("ApplyProperties: skipping non proper key: " + k.getClass().getName() + "[" + k + "]");
continue;
}
if (c.supportsProperty(key) && (key.bit & keyBits) != 0)
c.setProperty(key, editor.produceValue());
}
}
*/
private void extractMostRecentlyUsedStyles(LWMap map) {
final Collection<LWComponent> allNodes = map.getAllDescendents(LWMap.ChildKind.ANY);
final Map<Object,LWComponent> foundStyles = new HashMap();
Object typeToken;
LWComponent curStyle;
for (LWComponent c : allNodes) {
typeToken = typeToken(c);
if (typeToken == null)
continue;
curStyle = foundStyles.get(typeToken);
if (curStyle == null) {
// first type we've seen this type: just load it up
foundStyles.put(typeToken, c);
} else {
if (c.getNumericID() > curStyle.getNumericID()) {
// the object of this type is more recent than any we've seen before:
// stash away a reference to it.
foundStyles.put(typeToken, c);
}
}
}
for (Map.Entry<Object,LWComponent> e : foundStyles.entrySet()) {
final Object token = e.getKey();
final LWComponent lastCreated = e.getValue();
if (DEBUG.STYLE || DEBUG.INIT) out("extracted style for " + token + " from " + lastCreated);
final LWComponent extractedStyle = createStyle(lastCreated, token);
putStyle(token, extractedStyle);
}
}
private static Object typeToken(LWComponent c) {
return c.getTypeToken();
// This won't work for keeping separate slide styles until new objects created on the slide get
// their SLIDE_STYLE bit immediately set before they're auto-styled: will probably need a
// NewItem factory (may replace code that NodeTool currently has) fetchable from any LWContainer.
// Also, anywhere we use token == value would need to be updated, at least using this method
// of token differentiation, as it creates a new string object each time a SLIDE_STYLE token is
// fetched (could get around that with another HashMap...)
//
// if (c.hasFlag(Flag.SLIDE_STYLE))
// return c.getTypeToken() + "/slide";
// else
// return c.getTypeToken();
}
static void unregisterEditor(LWEditor editor) {
if (editor.getPropertyKey() == null) {
//if (DEBUG.Enabled) System.out.println("EditorManager: registration ignoring editor w/null key: " + dumpEditor(editor));
Log.debug("unregistration ignoring editor w/null key: " + dumpEditor(editor));
return;
}
if (mEditors.remove(editor)) {
if (DEBUG.TOOL || DEBUG.INIT || !VUE.isStartupUnderway()) out("UNREGISTERED EDITOR: " + editor);
if (editor instanceof java.awt.Component)
((java.awt.Component)editor).removePropertyChangeListener(singleton);
}
}
static boolean isRegistered(LWEditor editor)
{
if (editor.getPropertyKey() == null) {
//if (DEBUG.Enabled) System.out.println("EditorManager: registration ignoring editor w/null key: " + dumpEditor(editor));
Log.debug("ignoring editor w/null key: " + dumpEditor(editor));
return false;
}
if (mEditors.contains(editor))
return true;
else
return false;
}
static void registerEditor(LWEditor editor) {
if (editor.getPropertyKey() == null) {
//if (DEBUG.Enabled) System.out.println("EditorManager: registration ignoring editor w/null key: " + dumpEditor(editor));
Log.debug("registration ignoring editor w/null key: " + dumpEditor(editor));
return;
}
if (mEditors.add(editor)) {
// apparently things have changed such that starutp underway is done before most all
// the editors are registered -- don't think this should be a problem, but marking with DEBUG.WORK just in case
if (DEBUG.TOOL || DEBUG.INIT || (DEBUG.WORK && !VUE.isStartupUnderway()) ) out("REGISTERED EDITOR: " + editor);
Object curVal = null;
try {
curVal = editor.produceValue();
} catch (Throwable t) {
Log.warn("editor not ready to produce value: " + Util.tags(editor) + "; " + t);
}
if (curVal != null) {
//if (curVal != null && VUE.isStartupUnderway()) {
// doing this at runtime this can break us badly (LWText 0 font size props leaking to LWNodes!)
// Mike appears to have fixed the above problem, so we can re-enable this fix for VUE-1529 (aka VUE-408)
recordPropertyChangeInStyles("register",
editor.getPropertyKey(),
curVal,
true);
}
if (editor instanceof java.awt.Component)
((java.awt.Component)editor).addPropertyChangeListener(singleton);
} else
System.out.println(" REGISTERED AGAIN: " + editor);
}
private void findEditors() {
new EventRaiser<LWEditor>(this, LWEditor.class) {
public void dispatch(LWEditor editor) {
registerEditor(editor);
}
}.raise();
new EventRaiser<JLabel>(this, JLabel.class) {
public void dispatch(JLabel label) {
java.awt.Component gui = label.getLabelFor();
if (gui != null && gui instanceof LWEditor)
mLabels.put((LWEditor)gui, label);
}
}.raise();
Log.debug(this + " now managing " + mEditors.size() + " LWEditors.");
}
static void out(Object o) {
//Log.forcedLog("FOO", org.apache.log4j.Level.ALL, "EditorManager: " + o, null);
Log.debug(o);
}
public String toString() {
return "EditorManager";
}
}
/*
* So, can we meaningfully support the workflow of having nothing selected,
* changing the tool states, then creating a new object of ANY arbitrary type?
* Holy crap -- I think there's a way to really "do what I mean" with this, but it's
* freakin hairy. So every type has it's own style stored which can be used to
* style newly created objects of that type, as well as become the default editor
* states if a VueTool that is tied to that object type is enabled (and something
* isn't selected -- that's an interesting case -- lets say a link is selected and
* you load the node tool -- we probably want the selection to have priority in the
* editor states, and just ignore toe VueTool in this case -- tho actually, hell, it
* would be MUCH cleaner to just de-select what's selected when you load a VueTool
* if it doesn't happen to match the selection type of the VueTool).
* So anyway, what happens to the editor states if NOTHING is selected? We could
* use the current VueTool type if there is one, or more accurately, leave it with
* the last type that was selected. Tho, say, if the node tool was selected, and we
* leave that in place, and they change the stroke color, then they create a link,
* the link will have the properties of the last time we had the link style state,
* not the current state. So, to deal with this, if NOTHING is selected (and all
* editors get enabled), we could apply any editor state changes to ALL the current
* typed styles, which is the only way we could capture all the changes anyway, as
* there's no such object in the system that supports every property (e.g., both
* link shape and a node shape).
* The advantage here is that no matter what change they make, they know it
* will apply to the creation of any future object. The only drawback
* is that they may not know they're changing the state for ALL future
* objects -- e.g., they change the fill to create a node, but hell,
* now new text objects are also going to be created with that same
* fill...
* So, we could get crazy and keep TWO versions of the style for every type, and
* when nothing is selected, update every one of these second "provisional" styles
* for each type, and then the next object that's created, we know that's what they
* wanted, and then the provisional style for that type is copied over the in-use
* style for that type, and the provisional styles for all other types are thrown
* out (well, actually, just ignored -- they'll always be updating when nothing is
* selected, it's just that whenever a new object is created when nothing
* is selected and every editor is active, the style type of the object created
* gets updated with it's provisional style).
* Now, this sounds complicated enough that it might actually completely
* break some other workflow...
* So now lets say we select the node tool: currently, the tools still allow you to
* single select objects of any type (the only limit the drag selection) -- would
* could change that for simplicity. So lets say node tool is selected -- we want
* to load the node style... crap -- whan happens if they load the node style, but
* then forget the node tool is active, change the stroke style, and create a new
* link, thinking it will affect the link? Ahah -- taken care of: if a node
* was actually selected, we'll have the node style, and if they change
* a property they immediately see the change -- it's not for future use.
* But if nothing is selected, it's for future use -- however, when the
* node tool loads it could still UPDATE the provisional style with
* it's typed style!
* So, the rules this establishes are:
* (1) Whatever is selected always determines the typed style loaded into
* the editors.
* (2) When nothing is selected, we're ALWAYS working with the provisional style,
* even if a tool with a type is selected -- so changing the active tool is going to
* have NO EFFECT on the editor states, because even if the node tool is active,
* they could still create a link, and if they change the provisional style, we want
* that to effect the created link, not the next created node. If they have the
* node tool selected, then change an editor state (e.g., line color: red), then
* create a link, the provisional style updates the link style, the link gets
* selected, the link style is loaded. Then they click using the node tool to
* create a node: the selection is cleared, the provisional style is returned to the
* editor states (everything is enabled). Does the node get the red border
* color? If the prov style was "given" to the the link, then no, but then
* we just created a new object that didn't match the editor state.
* Maybe this would be easier if the active tool only allowed the editor
* states for it's type, even when nothing is selected. This at least
* provides some clarity, but then a new object (the link) could be created
* not matching the tool state...
* More complexity: if we want to load the typed style with the tool, which nicely
* communicates the avail properties for that type, yet it has properties that
* overlap with other tools, the typed style for the current tool could itself serve
* as yet another provisional style for the other styles: so when red line was
* selected int he node style, then a link is created, the link style
* could copy over the node style, but we'd ONLY want the "red" property,
* the one that changed, not every other property! Can we know that this
* happened? What if EVERY SINGLE EDITOR STATE CHANGE updated ALL the
* provisinal styles? This will work fine as long as the provisionals
* are tossed: e.g., you set editor states perfect for your next node,
* and created it: the link provisional style should be reset to
* the link style: it only gets used if they actually create a link.
* So, even if something is selected, the provisional styles are updated.
* But it's CRUCIAL that they get cleared when a new object is created,
* because if you just set everything up for a node, then create one,
* you don't want all that applying to youer next link, right?
* So all of this also means that all new on-map user object creation will have to
* go through the editor manager, tho actually the generic applyCurrentProperties is
* what does this, tho it would benefit from a more powerful name to suggest what
* we're doing: e.g., applyAppropriateProperties / applyDesiredStyle /
* popAndApplyEditorStates, something like that. Oh, that may not be enough tho
* because it's crucial that this is called so we know what to do with the
* provisional styles, which will cause mass confusion if we ever create an object
* without telling the editor manager that this is where the provisional style went.
* WHEN THE ACTIVE TOOL CHANGES, AND NOTHING IS SELECTED: We still leave everything
* enabled, as they might still want to style and create anything, however, we CAN
* load the editor display VALUES with the type for the tool if there is one, so
* they can at least see what they're working with. HOWEVER, if there were
* provisional properties available (editor states changed but not "used up"),
* we should resolve the provisional changes to the type of that editor
* if it has one, so that, say if going from the node tool to the link
* tool, especially the temporary link tool, the provisional property
* will be be resolved to the link style, for the link you're about to
* create.
*
*/