/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2016 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 3 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 3 for more details. * * You should have received a copy of the GNU General Public License version 3 * along with this work; if not, see http://www.gnu.org/licenses/ * * * Please visit http://neilcsmith.net if you need additional information or * have any questions. */ package net.neilcsmith.praxis.live.pxr; import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.AbstractAction; import javax.swing.Action; import net.neilcsmith.praxis.core.Argument; import net.neilcsmith.praxis.core.ArgumentFormatException; import net.neilcsmith.praxis.core.CallArguments; import net.neilcsmith.praxis.core.ComponentAddress; import net.neilcsmith.praxis.core.ComponentType; import net.neilcsmith.praxis.core.ControlAddress; import net.neilcsmith.praxis.core.info.ArgumentInfo; import net.neilcsmith.praxis.core.info.ComponentInfo; import net.neilcsmith.praxis.core.info.ControlInfo; import net.neilcsmith.praxis.core.interfaces.ComponentInterface; import net.neilcsmith.praxis.core.types.PString; import net.neilcsmith.praxis.gui.ControlBinding; import net.neilcsmith.praxis.live.core.api.Callback; import net.neilcsmith.praxis.live.core.api.Syncable; import net.neilcsmith.praxis.live.model.ComponentProxy; import net.neilcsmith.praxis.live.model.ProxyException; import net.neilcsmith.praxis.live.properties.PraxisProperty; import net.neilcsmith.praxis.live.pxr.api.Attributes; import net.neilcsmith.praxis.live.util.ArgumentPropertyAdaptor; import org.openide.nodes.Node; import org.openide.util.Exceptions; import org.openide.util.Lookup; import org.openide.util.lookup.Lookups; import org.openide.windows.TopComponent; /** * * @author Neil C Smith (http://neilcsmith.net) */ public class PXRComponentProxy implements ComponentProxy { private final static Logger LOG = Logger.getLogger(PXRComponentProxy.class.getName()); private final static Registry registry = new Registry(); private final Map<String, String> attributes; private final Set<Object> syncKeys; private final PropertyChangeSupport pcs; private final ComponentType type; private final boolean dynamic; private final InfoProperty infoProp; private final Lookup lookup; PXRProxyNode node; private PXRContainerProxy parent; private ComponentInfo info; private Map<String, BoundArgumentProperty> properties; private PropPropListener propertyListener; private List<Action> triggers; private List<Action> propActions; private EditorAction editorAction; boolean syncing; // private int listenerCount = 0; boolean nodeSyncing; boolean parentSyncing; private ArgumentPropertyAdaptor.ReadOnly dynInfoAdaptor; PXRComponentProxy(PXRContainerProxy parent, ComponentType type, ComponentInfo info) { this.parent = parent; this.type = type; this.info = info; attributes = new LinkedHashMap<>(); syncKeys = new HashSet<>(); pcs = new PropertyChangeSupport(this); dynamic = info.getProperties().getBoolean(ComponentInfo.KEY_DYNAMIC, false); infoProp = new InfoProperty(); lookup = createLookup(); } private Lookup createLookup() { return Lookups.fixed(new Sync(), new Attr()); } List<? extends PraxisProperty<?>> getProxyProperties() { return Collections.singletonList(infoProp); } private void initProperties() { assert EventQueue.isDispatchThread(); if (propertyListener == null) { propertyListener = new PropPropListener(); } ComponentAddress cmpAd = getAddress(); Map<String, BoundArgumentProperty> oldProps; // properties might not be null if called from dynamic listener if (properties == null) { oldProps = Collections.emptyMap(); } else { oldProps = properties; } // if (!oldProps.isEmpty()) { // for (BoundArgumentProperty prop : oldProps.values()) { // ((BoundArgumentProperty) prop).dispose(); // } // oldProps.clear(); // } properties = new LinkedHashMap<>(); File workingDir = getRoot().getWorkingDirectory(); for (String ctlID : info.getControls()) { ControlInfo ctl = info.getControlInfo(ctlID); BoundArgumentProperty prop = oldProps.remove(ctlID); if (prop != null && prop.getInfo().equals(ctl)) { // existing properties.put(ctlID, prop); continue; } ControlAddress address = ControlAddress.create(cmpAd, ctlID); prop = createPropertyForControl(address, ctl); if (prop != null) { ((BoundArgumentProperty) prop).addPropertyChangeListener(propertyListener); prop.setValue("address", address); prop.setValue("workingDir", workingDir); prop.setValue("componentInfo", info); properties.put(ctlID, prop); } } if (!oldProps.isEmpty()) { for (PraxisProperty<?> prop : oldProps.values()) { ((BoundArgumentProperty) prop).dispose(); } oldProps.clear(); } if (syncing) { setPropertiesSyncing(true); } } private void initDynamic() { LOG.finest("Setting up dynamic component adaptor"); dynInfoAdaptor = new ArgumentPropertyAdaptor.ReadOnly(this, "info", true, ControlBinding.SyncRate.None); dynInfoAdaptor.addPropertyChangeListener(new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { try { // refreshInfo((ComponentInfo) evt.getNewValue()); refreshInfo(ComponentInfo.coerce((Argument)evt.getNewValue())); } catch (ArgumentFormatException ex) { LOG.log(Level.WARNING, "", ex); } } }); PXRHelper.getDefault().bind(ControlAddress.create(getAddress(), ComponentInterface.INFO), dynInfoAdaptor); } void refreshInfo(ComponentInfo info) { if (this.info.equals(info)) { // should happen once on first sync? LOG.finest("Info is current"); return; } LOG.finest("Info changed - revalidating"); this.info = info; initProperties(); triggers = null; propActions = null; if (node != null) { node.refreshProperties(); node.refreshActions(); } // if (parent != null) { // parent.revalidate(this); // } firePropertyChange(ComponentInterface.INFO, null, null); } boolean isDynamic() { return dynamic; } @Override public ComponentAddress getAddress() { return parent == null ? null : parent.getAddress(this); } @Override public PXRContainerProxy getParent() { return parent; } @Override public ComponentType getType() { return type; } @Override public ComponentInfo getInfo() { return info; } @Override public Node getNodeDelegate() { if (node == null) { // node = new PXRProxyNode(this, getRoot().getSource()); node = new PXRProxyNode(this); } return node; } @Deprecated public void setAttribute(String key, String value) { setAttr(key, value); } void setAttr(String key, String value) { if (value == null) { attributes.remove(key); } else { attributes.put(key, value); } } @Deprecated public String getAttribute(String key) { return getAttr(key); } String getAttr(String key) { return attributes.get(key); } @Deprecated public String[] getAttributeKeys() { return getAttrKeys(); } String[] getAttrKeys() { return attributes.keySet().toArray(new String[attributes.size()]); } @Override public void addPropertyChangeListener(PropertyChangeListener listener) { pcs.addPropertyChangeListener(listener); } @Override public void removePropertyChangeListener(PropertyChangeListener listener) { pcs.removePropertyChangeListener(listener); } void firePropertyChange(String property, Object oldValue, Object newValue) { pcs.firePropertyChange(property, oldValue, newValue); if (node != null) { node.propertyChange(property, oldValue, newValue); } } public void call(String control, CallArguments args, final Callback callback) throws ProxyException { try { ControlAddress to = ControlAddress.create(getAddress(), control); PXRHelper.getDefault().send(to, args, callback); } catch (Exception ex) { throw new ProxyException(ex); } } List<Action> getTriggerActions() { if (triggers == null) { initTriggerActions(); } return triggers; } private void initTriggerActions() { triggers = new ArrayList<>(); for (String ctlID : info.getControls()) { ControlInfo ctl = info.getControlInfo(ctlID); if (ctl.getType() == ControlInfo.Type.Action) { triggers.add(new TriggerAction(ctlID)); } } triggers = Collections.unmodifiableList(triggers); } List<Action> getPropertyActions() { if (propActions == null) { initPropertyActions(); } return propActions; } private void initPropertyActions() { if (properties == null) { initProperties(); } propActions = new ArrayList<>(); BoundCodeProperty code = null; for (BoundArgumentProperty prop : properties.values()) { if (prop instanceof BoundCodeProperty) { // @TODO add proper key for this if ("code".equals(prop.getName())) { code = (BoundCodeProperty) prop; continue; } propActions.add(((BoundCodeProperty) prop).getEditAction()); propActions.add(((BoundCodeProperty) prop).getResetAction()); } } if (code != null) { if (!propActions.isEmpty()) { propActions.add(null); //separator } propActions.add(code.getEditAction()); propActions.add(code.getResetAction()); } propActions = Collections.unmodifiableList(propActions); } Action getEditorAction() { if (editorAction == null) { editorAction = new EditorAction(); } return editorAction; } public String[] getPropertyIDs() { if (properties == null) { initProperties(); } return properties.keySet().toArray(new String[0]); } public BoundArgumentProperty getProperty(String id) { if (properties == null) { initProperties(); } return properties.get(id); } protected BoundArgumentProperty createPropertyForControl(ControlAddress address, ControlInfo info) { if (isProxiedProperty(address.getID())) { return null; } if (info.getType() != ControlInfo.Type.Property && info.getType() != ControlInfo.Type.ReadOnlyProperty) { return null; } ArgumentInfo[] args = info.getOutputsInfo(); if (args.length != 1) { return null; } if (args[0].getType() == PString.class) { String mime = args[0].getProperties().getString(PString.KEY_MIME_TYPE, null); // if (BoundCodeProperty.isSupportedMimeType(mime)) { if (mime != null) { return new BoundCodeProperty(address, info, mime); } // } } return new BoundArgumentProperty(address, info); } protected boolean isProxiedProperty(String id) { return ComponentInterface.INFO.equals(id); } PXRRootProxy getRoot() { return parent.getRoot(); } void dispose() { if (LOG.isLoggable(Level.FINE)) { LOG.log(Level.FINE, "Dispose called on {0}", getAddress()); } parent = null; if (dynInfoAdaptor != null) { PXRHelper.getDefault().unbind(dynInfoAdaptor); } if (editorAction != null && editorAction.editor != null) { editorAction.editor.dispose(); } if (properties == null) { return; } for (PraxisProperty<?> prop : properties.values()) { if (prop instanceof BoundArgumentProperty) { LOG.log(Level.FINE, "Calling dispose on {0} property", prop.getName()); ((BoundArgumentProperty) prop).dispose(); } } properties = null; } private void setNodeSyncing(boolean sync) { assert EventQueue.isDispatchThread(); nodeSyncing = sync; if (LOG.isLoggable(Level.FINE)) { LOG.log(Level.FINE, "Setting node syncing {0} on {1}", new Object[]{sync, getAddress()}); } checkSyncing(); } void setParentSyncing(boolean sync) { if (parentSyncing != sync) { parentSyncing = sync; checkSyncing(); } } void checkSyncing() { boolean toSync = nodeSyncing || !syncKeys.isEmpty(); if (toSync != syncing) { syncing = toSync; if (LOG.isLoggable(Level.FINE)) { LOG.log(Level.FINE, "Setting properties syncing {0} on {1}", new Object[]{toSync, getAddress()}); } setPropertiesSyncing(toSync); } if (dynamic) { if (dynInfoAdaptor == null) { initDynamic(); } if (syncing || parentSyncing) { dynInfoAdaptor.setSyncRate(ControlBinding.SyncRate.Low); if (LOG.isLoggable(Level.FINE)) { LOG.log(Level.FINE, "Setting info syncing {0} on {1}", new Object[]{true, getAddress()}); } } else { dynInfoAdaptor.setSyncRate(ControlBinding.SyncRate.None); if (LOG.isLoggable(Level.FINE)) { LOG.log(Level.FINE, "Setting info syncing {0} on {1}", new Object[]{false, getAddress()}); } } } } private void setPropertiesSyncing(boolean sync) { if (properties == null) { return; } for (PraxisProperty<?> prop : properties.values()) { if (prop instanceof BoundArgumentProperty) { ((BoundArgumentProperty) prop).setSyncing(sync); } } } @Override public Lookup getLookup() { return lookup; } private class PropPropListener implements PropertyChangeListener { @Override public void propertyChange(PropertyChangeEvent evt) { // dispatch to our listeners and node's listeners firePropertyChange(evt.getPropertyName(), null, null); } } private class Sync implements Syncable { @Override public void addKey(Object key) { if (key == null) { throw new NullPointerException(); } syncKeys.add(key); checkSyncing(); } @Override public void removeKey(Object key) { if (syncKeys.remove(key)) { checkSyncing(); } } } private class Attr implements Attributes { @Override public void setAttribute(String key, String value) { setAttr(key, value); } @Override public String getAttribute(String key) { return getAttr(key); } } // private class DynPropListener implements PropertyChangeListener { // // @Override // public void propertyChange(PropertyChangeEvent evt) { // // info changed // LOG.finest("Info changed event"); // // // } // } private class InfoProperty extends PraxisProperty<ComponentInfo> { private InfoProperty() { super(ComponentInfo.class); setName(ComponentInterface.INFO); } @Override public ComponentInfo getValue() { return info; } @Override public boolean canRead() { return true; } } private class TriggerAction extends AbstractAction { private String control; TriggerAction(String control) { super(control); this.control = control; } @Override public void actionPerformed(ActionEvent ae) { try { call(control, CallArguments.EMPTY, new Callback() { @Override public void onReturn(CallArguments args) { // do nothing } @Override public void onError(CallArguments args) { // ??? } }); } catch (ProxyException ex) { Exceptions.printStackTrace(ex); } } } private class EditorAction extends AbstractAction { private PXRComponentEditor editor; EditorAction() { super("Edit..."); } @Override public void actionPerformed(ActionEvent ae) { if (editor == null) { editor = new PXRComponentEditor(PXRComponentProxy.this); } editor.show(); } } private static class Registry implements PropertyChangeListener { private List<PXRComponentProxy> syncing; public Registry() { syncing = new ArrayList<PXRComponentProxy>(); TopComponent.getRegistry().addPropertyChangeListener(this); } @Override public void propertyChange(PropertyChangeEvent evt) { try { assert EventQueue.isDispatchThread(); if (TopComponent.Registry.PROP_ACTIVATED_NODES.equals(evt.getPropertyName())) { ArrayList<PXRComponentProxy> tmp = new ArrayList<PXRComponentProxy>(); Node[] nodes = TopComponent.getRegistry().getActivatedNodes(); if (LOG.isLoggable(Level.FINEST)) { LOG.log(Level.FINEST, "Activated nodes = {0}", Arrays.toString(nodes)); } for (Node node : nodes) { PXRComponentProxy cmp = node.getLookup().lookup(PXRComponentProxy.class); if (cmp != null) { tmp.add(cmp); } } syncing.removeAll(tmp); for (PXRComponentProxy cmp : syncing) { cmp.setNodeSyncing(false); } syncing.clear(); syncing.addAll(tmp); for (PXRComponentProxy cmp : syncing) { cmp.setNodeSyncing(true); } tmp.clear(); } } catch (Exception e) { Exceptions.printStackTrace(e); } } } }