/*
* 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.ui;
import tufts.vue.*;
import tufts.vue.ActiveEvent;
import tufts.vue.MouseAdapter;
import static tufts.vue.LWComponent.Flag.*;
import static tufts.vue.LWComponent.HideCause.*;
import static tufts.vue.LWComponent.ChildKind;
import static tufts.vue.LWComponent.Order;
import tufts.vue.LWMap.Layer;
import tufts.vue.gui.*;
import tufts.Util;
import static tufts.Util.reverse;
import java.util.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import javax.swing.*;
import javax.swing.border.*;
import edu.tufts.vue.metadata.action.SearchAction;
/**
* @version $Revision: 1.84 $ / $Date: 2010-02-03 19:16:31 $ / $Author: mike $
* @author Scott Fraize
*/
public class LayersUI extends tufts.vue.gui.Widget implements LWComponent.Listener, LWSelection.Listener//, ActionListener
{
private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(LayersUI.class);
private static final boolean SCROLLABLE = true;
private final java.util.List<Row> mRows = new java.util.ArrayList();
private final JPanel mToolbar = new JPanel();
private final JPanel mRowList = new JPanel();
private final AbstractButton mShowAll = new JToggleButton(VueResources.getString("botton.layer.showall"));
// PROBLEM: IF WE ALLOW LAYERS IN THE SELECTION, LWComponent.selctedOrParent will
// start returning TRUE for anything inside the layer. This screws up
// hierarchy actions, delete, duplicate, etc.
// private final LWSelection mSelection = new LWSelection() {
// @Override
// protected boolean isSelectable(LWComponent c) {
// return c instanceof LWMap.Layer;
// }
// @Override
// protected void postNotify() { /* do nothing */ }
// @Override
// public String toString() {
// return "LayerSelection[" + paramString() + "]";
// }
// };
private LWMap mMap;
private boolean isDragUnderway;
private Row mDragRow;
private GridBagLayout gridbag = new GridBagLayout();
private GridBagConstraints gBC = new GridBagConstraints();
private int selectedIndex = -1;
//private static final Collection<VueAction> _selectionWatchers = new java.util.ArrayList();
private static final Collection<LayerAction> AllLayerActions = new java.util.ArrayList();
private JPopupMenu popupMenu = new JPopupMenu();
private JPopupMenu unlockPopupMenu = new JPopupMenu();
private JMenuItem unLockMenuItem;
private JMenuItem renameMenuItem;
private JMenuItem duplicateMenuItem;
private JMenuItem lockMenuItem;
private JMenuItem deleteMenuItem;
//private class LayerAction extends Actions.LWCAction {
private class LayerAction extends VueAction {
// @Override
// protected Collection<VueAction> getSelectionWatchers() {
// return _selectionWatchers;
// }
Layer active;
@Override public boolean overrideIgnoreAllActions() { return true; }
LayerAction(String name, String tip) {
//super(name, tip, KeyStroke.getKeyStroke(KeyEvent.VK_L, 0), null);
super(name, tip, null, null);
AllLayerActions.add(this);
}
@Override
public void actionPerformed(ActionEvent ae) {
active = getActiveLayer();
super.actionPerformed(ae);
}
// protected java.util.List<LWComponent> selection() {
// return null;
// }
//@Override boolean enabledFor(LWSelection s) { return true; }
@Override protected boolean enabled() { return true; }
@Override
protected LWSelection selection() {
return null;
//return mSelection;
}
// // This is overridden only because VueAction.enabledFor is
// // *package* private, instead of protected, and we're
// // not going to change that right now, so we need a
// // new enabled method we can override.
// @Override
// protected void updateEnabled(LWSelection selection) {
// if (selection == null)
// setEnabled(false);
// else
// setEnabled(enabledWith(selection));
// }
// boolean enabledWith(LWSelection s) { return s.size() > 0; }
boolean enabledWith(Layer layer) { return layer != null; }
// this is called at the end of each action execution
@Override
protected void updateSelectionWatchers() {
super.updateSelectionWatchers();
updateLayerActionEnabled(getActiveLayer());
}
@Override
public String getUndoName(ActionEvent e, Throwable exception) {
String name = super.getUndoName(e, exception) + " Layer";
//if (selection().only() instanceof Layer && (this == LAYER_DUPLICATE || this == LAYER_DELETE))
if (active != null && (this == LAYER_DUPLICATE || this == LAYER_DELETE))
name += " " + Util.quote(active.getDisplayLabel());
//name += " " + Util.quote(selection().only().getDisplayLabel());
return name;
}
}
static void updateLayerActionEnabled(Layer layer) {
for (LayerAction a : AllLayerActions)
a.setEnabled(a.enabledWith(layer));
}
private static int NewLayerCount = 1;
// private final VueAction
// LAYER_NEW = new VueAction("New", "Create a new layer", null, null) {
// public void act() {
// mMap.addLayer("New Layer " + NewLayerCount++);
// }
// @Override
// public String getUndoName() { return "New Layer"; }
// };
private final LayerAction
LAYER_NEW = new LayerAction(VueResources.getString("layer.new"), VueResources.getString("layer.createnewlayer")) {
@Override
boolean enabledWith(Layer layer) {
return mMap != null;
}
public void act() {
mMap.addLayer(VueResources.getString("layer.newlayer") + NewLayerCount++);
}
@Override
public String getUndoName() { return VueResources.getString("layer.newlayer"); }
},
LAYER_DUPLICATE = new LayerAction(VueResources.getString("layer.duplicate"), VueResources.getString("layer.duplicatelayer")) {
// public void act() {
// for (LWComponent c : reverse(selection()))
// mMap.addChild(c.duplicate());
// }
public void act() {
final Layer dupe = (Layer) active.duplicate();
mMap.addOnTop(active, dupe);
setActiveLayer(dupe); // make the new duplicate layer the active layer
}
},
LAYER_DELETE = new LayerAction(VueResources.getString("layer.delete"), VueResources.getString("layer.removeall")) {
@Override
boolean enabledWith(Layer layer) {
return mRows.size() > 1;
}
@Override
public void act() {
if (active.numChildren() > 0) {
String message = String.format(Locale.getDefault(),VueResources.getString("layer.delete.label"),
active.getLabel());
if (VueUtil.confirm(null,
message, VueResources.getString("dialog.title.confirmation"),
JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION)
return;
}
selectedIndex = active.getIndex();
mMap.deleteChildPermanently(active); // todo: LWMap should setActiveLayer(null) if active is deleted
mMap.setActiveLayer(null);
//setActiveLayer(null, true);
attemptAlternativeActiveLayer(true); // better if this tried to find the nearest layer, and not check last-active
}
},
LAYER_MERGE_DOWN = new LayerAction(VueResources.getString("layer.mergedown"), VueResources.getString("layer.mergeintolayer")) {
// @Override
// boolean enabledWith(LWSelection s) {
// return s.size() == 1
// && s.first() != mRows.get(mRows.size()-1).layer;
// }
@Override
boolean enabledWith(Layer layer) {
return layer != null && indexOf(layer) < mRows.size() - 1;
}
@Override
public void act() {
//final LWComponent mergingDown = selection().first();
final LWContainer mergingDown = active;
final Layer below = (Layer) mRows.get(indexOf(mergingDown) + 1).layer;
below.takeAllChildren(mergingDown);
setActiveLayer(below);
mMap.deleteChildPermanently(mergingDown);
}
},
LAYER_FILTER = new LayerAction(VueResources.getString("layer.filter"), VueResources.getString("layer.hideunselected")) {
boolean flg = true;
@Override
boolean enabledWith(Layer layer) {
return true;
}
@Override
public void act() {
if(flg){
((JButton)mToolbar.getComponent(3)).setBorderPainted(true);
((JButton)mToolbar.getComponent(3)).setBorderPainted(true);
((JButton)mToolbar.getComponent(3)).setContentAreaFilled(false);
((JButton)mToolbar.getComponent(3)).setIcon(VueResources.getImageIcon("layer.filter.on"));
((JButton)mToolbar.getComponent(3)).setBorder(BorderFactory.createEtchedBorder(1));
//super.updateSelectionWatchers();
Layer layer = getActiveLayer();
Row row =null;
for (Row rows : mRows){
row = rows;
if (rows.layer.equals(layer)){
row.layer.setVisible(true);
}else{
row.layer.setVisible(false);
}
}
//locked.setEnabled(layer.isVisible());
//label.setEnabled(layer.isVisible());
VUE.getSelection().setTo(row.layer.getAllDescendents());
VUE.getMainWindow().repaint();
if (row.layer == getActiveLayer() && !canBeActive(row.layer))
if (AUTO_ADJUST_ACTIVE_LAYER) attemptAlternativeActiveLayer(false);
setActiveLayer((Layer) layer, !UPDATE);
flg = !flg;
}else{
((JButton)mToolbar.getComponent(3)).setBorderPainted(false);
((JButton)mToolbar.getComponent(3)).setContentAreaFilled(false);
((JButton)mToolbar.getComponent(3)).setIcon(VueResources.getImageIcon("layer.filter.off"));
((JButton)mToolbar.getComponent(3)).setRolloverIcon(VueResources.getImageIcon("layer.filter.on"));
active = getActiveLayer();
Row row =null;
for (Row rows : mRows){
row = rows;
row.layer.setVisible(true);
VUE.getSelection().setTo(row.layer.getAllDescendents());
}
flg = !flg;
}
}
},
LAYER_LOCK = new LayerAction(VueResources.getString("layer.lock"), VueResources.getString("layer.lock")) {
@Override
boolean enabledWith(Layer layer) {
return true;
}
@Override
public void act() {
}
}
// LAYER_MERGE = new LayerAction("Merge", "Merge multiple layers") {
// @Override
// boolean enabledWith(LWSelection s) { return s.size() > 1; }
// @Override
// public void act() {
// final Layer merged = new LWMap.Layer("Merged");
// final ArrayList<LWComponent> mergedChildren = new ArrayList();
// for (LWComponent c : reverse(selection())) {
// merged.takeAllChildren(c);
// mMap.deleteChildPermanently(c);
// }
// // for (LWComponent c : reverse(selection())) {
// // mergedChildren.addAll(c.getChildren());
// // if (c instanceof LWContainer)
// // ((LWContainer)c).setChildren(null);
// // mMap.deleteChildPermanently(c);
// // //merged.addChildren(c.getChildren(), LWComponent.ADD_PRESORTED);
// // //merged.addAll(c.getChildren());
// // }
// // merged.setChildren(mergedChildren);
// mMap.addChild(merged);
// }
// @Override
// public String getUndoName() { return "Merge Layers"; }
// },
;
public LayersUI() {
super("layers");
setName("layersUI");
mToolbar.setName("layersUI.tool");
mRowList.setName("layersUI.rows");
mRowList.setLayout(new GridBagLayout());
mRowList.addMouseListener(RowMouseEnterExitTracker); // doesn't work?
//To Change the GUI
gBC.fill = GridBagConstraints.HORIZONTAL;
addButton(LAYER_NEW);
addButton(LAYER_DUPLICATE);
//addButton(LAYER_MERGE);
addButton(LAYER_MERGE_DOWN);
addButton(LAYER_FILTER);
//addButton(LAYER_LOCK);
addButton(LAYER_DELETE);
gBC.weightx = 0.5;
gBC.gridx = 1;
gBC.gridy = 0;
JPanel panel = new JPanel(){
public void paintComponent(Graphics g)
{
// call paintComponent to ensure the panel displays correctly
super.paintComponent(g);
g.drawLine(10, 5, 10, 25);
}
};
panel.setForeground(Color.gray);
panel.setPreferredSize(new Dimension(10,25));
mToolbar.add(panel,gBC);
// if (DEBUG.Enabled) {
// mShowAll.addActionListener(new ActionListener() {
// public void actionPerformed(ActionEvent e) {
// loadLayers(mMap);
// }});
// addButton(mShowAll);
// addButton(Actions.Group);
// addButton(Actions.Undo);
// }
mToolbar.setBorder(new SubtleSquareBorder(true));
add(mToolbar, BorderLayout.NORTH);
if (SCROLLABLE) {
JScrollPane sp = new JScrollPane(mRowList);
sp.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
//sp.setBorder(null);
add(sp, BorderLayout.CENTER);
sp.addMouseListener(RowMouseEnterExitTracker); // doesn't always work
} else {
//mRowList.setSize(300, 40);
//mRowList.setMaximumSize(new Dimension(300, 40));
add(mRowList, BorderLayout.NORTH);
}
// mSelection.addListener(new LWSelection.Listener() {
// public void selectionChanged(LWSelection s) {
// LayerAction.updateSelectionWatchers(_selectionWatchers, s);
// }
// @Override
// public String toString() {
// return "Handler for " + _selectionWatchers.size() + " " + LayerAction.class.getSimpleName() + "s";
// }
// });
VUE.addActiveListener(LWMap.class, this);
VUE.getSelection().addListener(this);
//VUE.addActiveListener(Layer.class, this);
//VUE.addActiveListener(LWComponent.class, this);
//setMinimumSize(new Dimension(300,260));
renameMenuItem = new JMenuItem(VueResources.getString("layer.rename"));
popupMenu.add(renameMenuItem);
popupMenu.addSeparator();
renameMenuItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
Layer layer = getActiveLayer();
Row row =null;
for (Row rows : mRows){
if (rows.layer.equals(layer))
row = rows;
}
if (row == null)
return;
row.label.setVisible(true);
row.label.setEnabled(true);
row.label.setEditable(true);
row.label.requestFocus();
}
});
unLockMenuItem = new JMenuItem(VueResources.getString("layer.unlock"));
unlockPopupMenu.add(unLockMenuItem);
unLockMenuItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
Layer layer = getActiveLayer();
Row row =null;
for (Row rows : mRows){
if (rows.layer.equals(layer))
row = rows;
}
if (row == null)
return;
row.locked.setSelected(false);
row.locked.setIcon(VueResources.getImageIcon("lockOpen"));
layer.setLocked(false);
}
});
duplicateMenuItem = new JMenuItem(VueResources.getString("layer.duplicate"));
popupMenu.add(duplicateMenuItem);
popupMenu.addSeparator();
duplicateMenuItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
Layer active = getActiveLayer();
final Layer dupe = (Layer) active.duplicate();
mMap.addOnTop(active, dupe);
setActiveLayer(dupe); // make the new duplicate layer the active layer
}
});
lockMenuItem = new JMenuItem(VueResources.getString("layer.lock"));
popupMenu.add(lockMenuItem);
popupMenu.addSeparator();
lockMenuItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
Layer layer = getActiveLayer();
Row row =null;
for (Row rows : mRows){
if (rows.layer.equals(layer))
row = rows;
}
if (row == null)
return;
row.locked.setSelected(true);
row.locked.setIcon(VueResources.getImageIcon("lock"));
layer.setLocked(true);
if (layer == getActiveLayer() && !canBeActive(layer))
if (AUTO_ADJUST_ACTIVE_LAYER) attemptAlternativeActiveLayer(false);
}
});
deleteMenuItem = new JMenuItem(VueResources.getString("layer.delete"));
popupMenu.add(deleteMenuItem);
deleteMenuItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
deleteMenuItem.setEnabled(true);
// Modal dialog with OK button
String message = VueResources.getString("dialog.deletelayer.message");
if (VueUtil.confirm(null,
message, VueResources.getString("dialog.title.confirmation"),
JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION){
Layer active = getActiveLayer();
if(!active.isLocked()){
selectedIndex = active.getIndex();
mMap.deleteChildPermanently(active); // todo: LWMap should setActiveLayer(null) if active is deleted
mMap.setActiveLayer(null);
attemptAlternativeActiveLayer(true); // better if this tried to find the nearest layer, and not check last-active
}
if(mRows.size() == 1){
deleteMenuItem.setEnabled(false) ;
}
}
}
});
}
private void addButton(Action a) {
addButton(new JButton(a));
}
private void addButton(AbstractButton b) {
if (b instanceof JToggleButton)
;//b.putClientProperty("JButton.buttonType", "roundRect"); // for Mac Leopard Java
else
b.putClientProperty("JButton.buttonType", "textured"); // for Mac Leopard Java
// Font defaultFont = getFont();
// Font boldFont = defaultFont.deriveFont(Font.BOLD);
// Font smallFont = defaultFont.deriveFont((float) boldFont.getSize()-2);
// b.setFont(smallFont);
b.setFont(tufts.vue.gui.GUI.LabelFace);
b.setFocusable(false);
if(b.getAction() == null) {
;
} else if(b.getAction().equals(LAYER_NEW)){
b.setText(VueResources.getString("layersui.newlayer.tooltip"));
b.setIcon(tufts.vue.VueResources.getImageIcon("metadata.editor.add.up"));
b.setRolloverEnabled(true);
b.setPreferredSize(new Dimension(90,30));
//b.setMinimumSize(new Dimension(90,30));
b.setRolloverIcon(VueResources.getImageIcon("metadata.editor.add.down"));
b.setBorder(BorderFactory.createEmptyBorder(2,5,2,2));
b.setHorizontalAlignment(JButton.LEADING); // optional
b.setBorderPainted(false);
b.setContentAreaFilled(false);
gBC.weightx = 0.5;
gBC.gridx = 0;
gBC.gridy = 0;
}else if(b.getAction().equals(LAYER_DUPLICATE)){
b.setText("");
b.setIcon(tufts.vue.VueResources.getImageIcon("layer.duplicate.add"));
b.setRolloverEnabled(true);
b.setRolloverIcon(VueResources.getImageIcon("layer.duplicate.add.ov"));
//b.setBorder(BorderFactory.createEmptyBorder(2,5,2,30));
//b.setHorizontalAlignment(JButton.LEADING); // optional
b.setBorderPainted(false);
b.setContentAreaFilled(false);
gBC.weightx = 0.5;
gBC.gridx = 2;
gBC.gridy = 0;
}else if(b.getAction().equals(LAYER_MERGE_DOWN)){
b.setText("");
b.setIcon(tufts.vue.VueResources.getImageIcon("layer.merge.add"));
b.setRolloverEnabled(true);
b.setRolloverIcon(VueResources.getImageIcon("layer.merge.add.ov"));
b.setBorder(BorderFactory.createEmptyBorder(0,0,0,0));
//b.setHorizontalAlignment(JButton.LEADING); // optional
b.setBorderPainted(false);
b.setContentAreaFilled(false);
gBC.weightx = 0.5;
gBC.gridx = 3;
gBC.gridy = 0;
}else if(b.getAction().equals(LAYER_DELETE)){
b.setText("");
b.setIcon(tufts.vue.VueResources.getImageIcon("ontologicalmembership.delete.up"));
b.setRolloverEnabled(true);
b.setRolloverIcon(VueResources.getImageIcon("ontologicalmembership.delete.down"));
b.setBorder(BorderFactory.createEmptyBorder(0,0,0,0));
//b.setHorizontalAlignment(JButton.LEADING); // optional
b.setBorderPainted(false);
b.setContentAreaFilled(false);
gBC.gridx = 6;
gBC.gridy = 0;
}else if(b.getAction().equals(LAYER_FILTER)){
b.setText("");
b.setIcon(tufts.vue.VueResources.getImageIcon("layer.filter.off"));
b.setRolloverEnabled(true);
b.setRolloverIcon(VueResources.getImageIcon("layer.filter.on"));
b.setBorder(BorderFactory.createEmptyBorder(1,1,1,1));
//b.setHorizontalAlignment(JButton.LEADING); // optional
b.setBorderPainted(false);
b.setContentAreaFilled(false);
gBC.gridx = 4;
gBC.gridy = 0;
}/*else if(b.getAction().equals(LAYER_LOCK)){
b.setText("");
b.setIcon(tufts.vue.VueResources.getImageIcon("lockOpen"));
b.setRolloverEnabled(true);
b.setRolloverIcon(VueResources.getImageIcon("lock"));
b.setBorder(BorderFactory.createEmptyBorder(0,0,0,0));
//b.setHorizontalAlignment(JButton.LEADING); // optional
b.setBorderPainted(false);
b.setContentAreaFilled(false);
gBC.gridx = 5;
gBC.gridy = 0;
} */
mToolbar.setLayout(gridbag);
mToolbar.add(b,gBC);
//b.addActionListener(this);
}
@Override
public void addNotify() {
super.addNotify();
//setMinimumSize(new Dimension(400,120+mToolbar.getHeight()));
SwingUtilities.getWindowAncestor(this).addMouseListener(RowMouseEnterExitTracker);
}
public void activeChanged(ActiveEvent e, LWMap map) {
loadMap(map);
}
// public void activeChanged(ActiveEvent e, Layer layer) {
// // used to just call indic
// }
// public void activeChanged(ActiveEvent e, LWComponent c) {
// enableForSingleSelection(c);
// // for debug / child-list mode:
// if (mMap != null && !mMap.isLayered())
// indicateActiveLayers(null);
// }
private static final boolean UPDATE = true;
private void setActiveLayer(Layer c) {
setActiveLayer(c, !UPDATE);
}
private void setActiveLayer(final Layer layer, boolean update) {
if (DEBUG.EVENTS) Log.debug("SET-ACTIVE: " + layer);
if (layer != null)
mMap.setClientData(Layer.class, "last", mMap.getActiveLayer());
mMap.setActiveLayer(layer);
if (update)
indicateActiveLayers(null);
else{
LWSelection selection = VUE.getSelection();
indicateActiveLayers(selection.getParents());
}
updateLayerActionEnabled(layer);
// if (DEBUG.Enabled) {
// GUI.invokeAfterAWT(new Runnable() { public void run() {
// VUE.setActive(Layer.class, LayersUI.this, layer);
// }});
// }
}
private boolean canBeActive(LWComponent layer) {
return canBeActive(layer, true);
}
private boolean canBeActive(LWComponent layer, boolean checkLocking) {
if (layer == null || layer.isHidden() || layer.isDeleted())
return false;
else if (checkLocking && layer.isLocked() && mRows.size() > 1)
return false;
else
return layer instanceof Layer;
}
private static final boolean AUTO_ADJUST_ACTIVE_LAYER = false;
private void attemptAlternativeActiveLayer(boolean isDeleteFlg) {
//if (!AUTO_ADJUST_ACTIVE_LAYER) return;
Layer lastActive = null;
final Layer curActive = getActiveLayer();
if(!isDeleteFlg){
lastActive = mMap.getClientData(Layer.class, "last");
}else{
if(selectedIndex > 0)
lastActive = (Layer) mMap.getChild(selectedIndex-1);
else{
lastActive = mMap.getClientData(Layer.class, "last");
}
}
if (canBeActive(lastActive)) {
setActiveLayer(lastActive, UPDATE);
return;
}
LWComponent fullyOpen = null;
// find the top-most visible and unlocked layer:
for (Row row : mRows)
if (canBeActive(row.layer))
fullyOpen = row.layer;
if (fullyOpen != null) {
setActiveLayer((Layer) fullyOpen, UPDATE);
return;
}
LWComponent visibleButLocked = null;
// find the top-most visible layer:
for (Row row : mRows)
if (canBeActive(row.layer, false))
visibleButLocked = row.layer;
if (visibleButLocked != null && (curActive == null || (curActive.isHidden() && curActive.isLocked()))) {
// only switch to visible but locked if the current active is actually worse off
setActiveLayer((Layer) visibleButLocked, UPDATE);
}
}
private Layer getActiveLayer() {
if (mMap == null) {
Log.warn("getActiveLayer when map is null");
return null;
} else {
return mMap.getActiveLayer();
}
}
public void selectionChanged(LWSelection s) {
// System.err.println("selectionChanged: " + s + "; size=" + s.size() + "; " + Arrays.asList(s.toArray()) + "; parents=" + s.getParents());
updateGrabEnabledForSelection(s);
// if (!s.getParents().contains(mMap.getActiveLayer()))
// for (LWComponent c : s.getParents())
// if (c instanceof Layer)
// mMap.setActiveLayer(c);
if (s.getParents().size() == 1) {
LWMap.Layer layer = s.first().getLayer();
if (layer == null) {
Log.info("ignoring null layer in single selection parent (presumably not a map-member yet)");
} else {
setActiveLayer(layer);
}
}
// if (s.size() == 1 && s.first().getLayer() != null) {
// //if (DEBUG.Enabled) Log.debug("selectionChanged: single selection; activate layer of: " + s.first());
// setActiveLayer(s.first().getLayer());
// } else if (s.getParents().size() == 1 && s.first().getParent() instanceof Layer) {
// //if (DEBUG.Enabled) Log.debug("selectionChanged: one parent in selection; active parent " + s.first().getParent());
// setActiveLayer((Layer) s.first().getParent());// }
indicateActiveLayers(s.getParents());
// // for debug / child-list mode:
// if (mMap != null && !mMap.isLayered())
// indicateActiveLayer(null);
}
private void loadMap(final LWMap map)
{
if (DEBUG.EVENTS) Log.debug("load map " + map);
if (mMap == map)
return;
if (mMap != null)
mMap.removeLWCListener(this);
mMap = map;
//setActiveLayer(map.getActiveLayer());
loadLayers(map);
if (map != null) {
// todo: we should be able to just listen for LWKey.HierarchyChanged, tho
// this currently is only generated on UNDO's, and hardly anything is
// currently listenting for it (OutlineViewTree, and some references to "hier.*)
map.addLWCListener(this);
}
}
private boolean mLayerReloadRequired = false;
public void LWCChanged(LWCEvent e) {
//if (DEBUG.EVENTS) Log.debug("handling " + e + "; source=" + e.getSource());
// ignore events from children: just want hierarchy events directly from the map
// (as we're only interested in changes to map layers)
if (e.key == LWKey.UserActionCompleted) {
if (mLayerReloadRequired)
loadLayers(mMap);
else
repaint(); // repaint the previews
mLayerReloadRequired = false;
}
else if (e.getSource() == mMap || mShowAll.isSelected()) {
// any hierarcy event on the map itself must involve layers
if (e.getName().startsWith("hier.")) {
if (e.getName().startsWith("hier.move.")) {
// this immediate reload is required for UI to update during drags
// of layer rows -- could also condition this based on the mouse
// being held down on a Row as opposed to know this involves
// hier.move.forward/backward events.
loadLayers(mMap);
} else {
mLayerReloadRequired = true;
if (DEBUG.EVENTS) Log.debug("TAGGED FOR RELOAD on " + e.key);
}
}
}
else if (e.key == LWKey.Deleting && e.getComponent() instanceof Layer) {
// failsafe only: should already have been handled by above "hier." case
mLayerReloadRequired = true;
if (DEBUG.EVENTS) Log.debug("TAGGED FOR RELOAD on " + e.key);
}
}
private final Color AlphaWhite = new Color(255,255,255,128);
private static final Color ActiveBG = VueResources.getColor("layerUI.activeLayer.background", Color.blue);
private final Color IncludedBG = Util.alphaMix(AlphaWhite, VueConstants.COLOR_SELECTION);
//private final Color IncludedBG = VueConstants.COLOR_SELECTION.darker();
//private final Color IncludedBG = new Color(128,128,255,128);
private final Color SelectedBG = VueConstants.COLOR_SELECTION.brighter();
private void indicateActiveLayers(Collection<LWContainer> parents) {
final Layer activeLayer = getActiveLayer();
if (parents == null) {
// update the active layer indication based on a change
// in the active layer -- not a change in a selection
// this will see to it that only one layer is indicated,
// and all other layers are not.
for (Row row : mRows) {
//row.activeIcon.setEnabled(row.layer == activeLayer);
if (row.layer.isSelected() || row.layer == activeLayer){
//if (row.layer.isSelected())
row.setBackground(row.layer instanceof Layer ? ActiveBG : SelectedBG);
}
else
row.setBackground(null);
// if (row.layer == activeLayer) {
// row.setBorder(BorderFactory.createLineBorder(Color.red, 2));
// }
// else {
// row.setBorder(new CompoundBorder(new MatteBorder(1,0,1,0, Color.lightGray),
// GUI.makeSpace(3,7,3,7)));
// }
}
} else {
// update the active layer indication based on a change
// in the selection (hilite *any* layers found in the selection)
final Set<Layer> layersInSelection = new HashSet(parents.size());
// on empty selection, parents will be empty
for (LWComponent c : parents)
layersInSelection.add(c.getLayer());
for (Row row : mRows) {
//row.activeIcon.setEnabled(row.layer == activeLayer);
if (row.layer instanceof Layer) {
if (layersInSelection.contains(row.layer)) {
//row.layer.setSelected(true);
if (row.layer == activeLayer){
row.setBackground(ActiveBG);
if(layersInSelection.size()>1){
row.setBorder(BorderFactory.createLineBorder(Color.blue, 2));
}
}
else{
row.setBackground(IncludedBG);
row.setBorder(new CompoundBorder(new MatteBorder(1,1,1,1, Color.lightGray),
GUI.makeSpace(3,7,3,7)));
//row.setBorder(null);
}
} else {
//row.layer.setSelected(false);
if (row.layer == activeLayer){
row.setBackground(ActiveBG);
if(layersInSelection.size()>1){
row.setBorder(BorderFactory.createLineBorder(Color.blue, 2));
}else{
row.setBorder(new CompoundBorder(new MatteBorder(1,1,1,1, Color.lightGray),
GUI.makeSpace(3,7,3,7)));
}
}
else{
row.setBackground(null);
row.setBorder(new CompoundBorder(new MatteBorder(1,1,1,1, Color.lightGray),
GUI.makeSpace(3,7,3,7)));
//row.setBorder(null);
}
}
} else if (row.layer.isSelected()) {
row.setBackground(SelectedBG);
} else{
row.setBackground(null);
}
}
}
}
private void updateGrabEnabledForSelection(LWSelection s) {
final Collection<LWContainer> parents = s.getParents();
//final LWContainer parent0 = parents.isEmpty() ? null : parents.iterator().next();
boolean disable =
s.size() < 1
|| s.only() instanceof Layer;
// todo: to be more precise, could always accumme related parets
if (!disable) {
boolean canExtract = false;
for (LWContainer parent : parents) {
if (isExtractableParent(parent)) {
canExtract = true;
break;
}
}
disable = !canExtract;
}
for (Row row : mRows) {
if (row.grab == null)
continue;
if (disable) {
row.grab.setEnabled(false);
} else if (parents.size() == 1 && parents.contains(row.layer)) {
//Log.debug("DISABLE GRAB IN " + row);
row.grab.setEnabled(false);
} else {
//Log.debug(" ENABLE GRAB IN " + row);
row.grab.setEnabled(true);
}
}
}
private static boolean isExtractableParent(LWContainer parent) {
return parent instanceof Layer
|| parent instanceof LWMap; // shouldn't happen, but just in case of up-leakage
// if (parent instanceof LWGroup)
// return false;
// else if (parent instanceof LWNode)
// return false;
// else if (parent instanceof LWSlide)
// return false;
// else
// return true;
}
private static boolean layerCanGrab(Layer layer, LWComponent c) {
final LWContainer parent = c.getParent();
if (parent == layer)
return false;
else
return isExtractableParent(parent);
}
private void grabFromSelection(Layer layer) {
final LWSelection selection = VUE.getSelection();
final java.util.List grabbing = new ArrayList();
for (LWComponent c : selection) {
if (layerCanGrab(layer, c))
grabbing.add(c);
}
// todo perf: remove all old layer in one swoop, then add to new
layer.addChildren(grabbing);
selection.resetStatistics();
selectionChanged(selection);
//indicateActiveLayers(selection.getParents());
}
private void loadLayers(final LWMap map) {
if (DEBUG.EVENTS) Log.debug("RELOADING LAYERS IN " + map);
mRows.clear();
if (map != null) {
// handle in reverse order (top layer on top)
for (LWComponent layer : reverse(map.getChildren())) {
mRows.add(produceRow(layer));
if (mShowAll.isSelected()) {
//for (LWComponent c : reverse(layer.getChildren()))
for (LWComponent c : reverse(layer.getAllDescendents(ChildKind.PROPER, new ArrayList<LWComponent>(), Order.DEPTH)))
mRows.add(produceRow(c));
}
}
updateLayerActionEnabled(map.getActiveLayer());
} else {
updateLayerActionEnabled(null);
}
if (!isDragUnderway) {
updateGrabEnabledForSelection(VUE.getSelection());
indicateActiveLayers(null);
}
layoutRows();
if (getActiveLayer() != null && getActiveLayer().isDeleted()) {
mMap.setActiveLayer(null);
attemptAlternativeActiveLayer(true);
}
}
private void layoutRows() {
layoutRows(mRowList, mRows);
}
private void layoutRows(final JComponent container, final java.util.List<? extends JComponent> rows)
{
GridBagConstraints c = new GridBagConstraints();
c.weighty = 0; // 1 has all expanding to fill vertical, 0 leaves all at min height
c.weightx = 1;
c.gridheight = 1;
c.gridwidth = 1;
//c.gridwidth = GridBagConstraints.REMAINDER;
//c.fill = GridBagConstraints.HORIZONTAL;
c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.NORTH;
c.gridx = 0;
c.gridy = 0;
// Each Row has a top and a bottom border line, so that
// one is always visible no matter what, but we normally only
// want to see a single line, so this will let them
// overlap during standard display (e.g., but not when drag-reordering)
c.insets = new Insets(0,0,-1,0);
container.removeAll();
if (!rows.isEmpty()) {
for (JComponent row : rows) {
c.insets.left = (((Row)row).layer.getDepth() - 1) * 56; // refactoring: note Row cast
row.setOpaque(true);
container.add(row, c);
c.gridy++;
}
if (c.weighty == 0) {
// now add a default vertical expander so the rest of items stay at the top
c.weighty = 1;
c.gridy = rows.size() + 1;
container.add(new JPanel(), c);
}
}
// will property event or DockWindow API: DockWindow controls this, and only polls it on init
//setMinimumSize(new Dimension(400,40*rows.size()));
container.revalidate(); // needed for Tiger (uneeded on Leopard)
////if (isVisible()) SwingUtilities.getWindowAncestor(this).pack();
container.repaint();
}
private Row produceRow(final LWComponent layer)
{
Row row = layer.getClientData(Row.class);
if (row != null) {
return row;
} else {
row = new Row(layer);
layer.setClientData(Row.class, row);
return row;
}
}
private int indexOf(final LWComponent layer) {
int i = 0;
for (Row row : mRows) {
if (row.layer == layer)
return i;
i++;
}
return -1;
}
// private boolean inExclusiveMode() {
// return fetchExclusiveRow() != null;
// }
private Row fetchExclusiveRow() {
return mMap.getClientData(Row.class, "exclusive");
}
private void storeExclusiveRow(Row row) {
mMap.setClientData(Row.class, "exclusive", row);
}
private Layer fetchPreExclusiveLayer() {
return mMap.getClientData(Layer.class, "pre-exclusive");
}
private void storePreExclusiveLayer(Layer layer) {
mMap.setClientData(Layer.class, "pre-exclusive", layer);
}
// TODO: if a row is deleted while in exclusive mode, you won't
// be able to access it's controls if it's un-deleted when NOT
// in exclusive mode -- you'll need to enter/exit exlusive mode
// to re-gain access (to visible/lock buttons)
private void setExclusiveMode(boolean entering, Row exclusiveRow)
{
//Log.debug("SET-EXCLUSIVE-MODE " + entering + "; nowExclusive=" + exclusiveRow);
if (entering)
storePreExclusiveLayer(getActiveLayer());
for (Row row : mRows) {
final LWComponent layer = row.layer;
if (entering) {
//layer.setFlag(WAS_LOCKED, layer.isLocked());
//layer.setFlag(WAS_HIDDEN, layer.isHidden(DEFAULT));
} else {
layer.setLocked(row.locked.isSelected());
layer.setHidden(DEFAULT, !row.visible.isSelected());
//layer.setLocked(layer.hasFlag(WAS_LOCKED));
//layer.setHidden(DEFAULT, layer.hasFlag(WAS_HIDDEN));
//layer.clearFlag(WAS_LOCKED);
//layer.clearFlag(WAS_HIDDEN);
}
row.visible.setEnabled(!entering);
row.locked.setEnabled(!entering);
if (row == exclusiveRow)
continue;
row.label.setEnabled(entering ? false : row.visible.isSelected());
layer.setHidden(LAYER_EXCLUDED, entering);
}
if (!entering) {
setActiveLayer(fetchPreExclusiveLayer(), true);
storeExclusiveRow(null);
storePreExclusiveLayer(null);
}
}
private static class TextEdit extends JTextField implements FocusListener {
static final boolean TransparentHack = Util.isMacLeopard();
static final Color Transparent = new Color(0,0,0,0);
static final Dimension MaxSize = new Dimension(Short.MAX_VALUE, 30);
// we'd like to use a shorter max-size to allow more room for the layer
// if you drag the window very big instead of the text-box, but
// this doesn't work with our hack that allows the variable width
// info field to not throw off row-to-row column alignment.
//static final Dimension MaxSize = new Dimension(200, 30);
// todo perf: these could be static
final Border activeBorder;
final Border inactiveBorder;
final Row row;
public TextEdit(final Row row) {
this.row = row;
setDragEnabled(false);
setPreferredSize(MaxSize);
setMaximumSize(MaxSize);
setText(row.layer.getDisplayLabel());
setToolTipText(row.layer.getDisplayLabel());
enableEvents(AWTEvent.MOUSE_EVENT_MASK);
enableEvents(AWTEvent.MOUSE_MOTION_EVENT_MASK);
addFocusListener(this);
addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent e) {
if (DEBUG.KEYS) Log.debug("KEY " + e);
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
//focusLost(null); // will be called again on actual focus-loss; could call setEditable(false);
setEditable(false); // rely's on focusLost being generated
}
}
});
row.layer.addLWCListener(new LWComponent.Listener() {
public void LWCChanged(LWCEvent e) {
final String text = row.layer.getDisplayLabel();
if (text.equals(getText())) {
// be sure to skip the setText if the text is the same:
// setting a text component to the same value tends to
// trigger a java bug where setScrollOffset(0) stops
// working, and the left-most text stays obscured if the
// text is wider than the display area.
return;
}
setText(text);
setScrollOffset(0);
// also do this later just in case: somes helps, tho
// Vista and XP still seem to have a hard time with this bug,
// but generally only if the model text is set elsewhere,
// and we're just handling a value update here.
GUI.invokeAfterAWT(new Runnable() { public void run() {
setScrollOffset(0);
}});
}},
LWKey.Label);
activeBorder = getBorder();
// Insets insets = activeBorder.getBorderInsets(this);
// if (Util.isMacLeopard()) {
// // sometimes this is wrong... would be safer to do this in addNotify
// insets.left -= 3;
// insets.right -= 3;
// }
// inactiveBorder = GUI.makeSpace(insets);
inactiveBorder = GUI.makeSpace(activeBorder.getBorderInsets(this));
//setOpaque(true);
setEditable(false);
}
private boolean isConstructed() {
return activeBorder != null;
}
@Override
public void setEnabled(boolean enabled) {
setForeground(enabled ? Color.black : Color.gray);
}
@Override
public void setEditable(boolean edit) {
if (isConstructed())
makeEditable(edit); // don't do this during default JTextComponent init
super.setEditable(edit);
}
public void focusGained(FocusEvent e) {}
public void focusLost(FocusEvent e) {
setEditable(false);
setScrollOffset(0);
row.layer.setLabel(getText().trim());
// make sure if text is longer than fits into field, we scroll back to 0 at the left
row.layer.getMap().getUndoManager().mark();
}
private void makeEditable(boolean edit) {
if (DEBUG.WORK) Log.debug("MAKE EDITABLE " + Util.tags(this) + " " + edit);
if (edit) {
setFocusable(true);
setBorder(activeBorder);
setBackground(Color.white);
if (!TransparentHack)
setOpaque(true);
} else {
setFocusable(false);
setBorder(inactiveBorder);
if (TransparentHack) {
setBackground(Transparent);
} else {
setBackground(null);
setOpaque(false);
}
}
}
// @Override
// public void addNotify() {
// // horizontal mouse-draggs across a non-edit mode label draw's some selection or
// // repaints chars with semi-transpareng BG, leading to blocky artifacts -- if this is
// // caret/hilighter, we should be able to turn it off or change the color, but having
// // had no success in that, it may just be a repaint issue much harder to fix (e.g.,
// // sometimes the text itself appears to "bolden" as it repaints)
// super.addNotify();
// //getCaret().setVisible(false);
// //getCaret().setSelectionVisible(false);
// setSelectionColor(Color.red);
// setHighlighter(null);
// //setCaret(null); // we'll get NPE
// }
@Override
protected void processEvent(AWTEvent e) {
// this form of delegation is much simpler than passing everything through
// our own mouse / mouse motion listeners, and has the added benefit of
// preventing horizontal cross-text drags from causing the blocky
// character-level repaint bug (when the background is semi-transparent)
if (!isEditable() && e instanceof MouseEvent) {
if (e.getID() == MouseEvent.MOUSE_CLICKED) {
mouseClicked((MouseEvent)e);
} else {
// This allows mouse press/release/drag events to
// be passed on to the Row as if they happened there.
// The coordinate system will be different, but
// Row.mouseDragged really only needs the relatve
// event-to-event deltas.
row.processEventUp(e);
}
} else
super.processEvent(e);
}
private void mouseClicked(MouseEvent e)
{
if (GUI.isDoubleClick(e)) {
if (DEBUG.MOUSE) Log.debug("DOUBLE CLICK " + this);
setEditable(true);
requestFocus();
}
}
public String toString() {
return "TextEdit[" + row + "]";
}
}
private static class Preview extends JPanel {
final LWComponent layer;
Preview(LWComponent c) {
layer = c;
}
@Override
public void paintComponent(Graphics _g) {
//final DrawContext dc = new DrawContext(DEBUG.BOXES ? g.create() : g, layer);
final DrawContext dc = new DrawContext(_g.create(), layer);
dc.setDraftQuality();
dc.setAntiAlias(true);
dc.setInteractive(false);
if (layer instanceof Layer == false) {
//dc.fillBackground(Color.white);
layer.drawFit(dc, 0);
return;
}
//System.out.println("bounds: " + Util.fmt(getBounds()));
//System.out.println(" clip: " + Util.fmt(g.getClipRect()));
// if (layer.isVisible()) {
// //g.setColor(Color.yellow);
// g.setColor(layer.getMap().getFillColor());
// ((Graphics2D)g).fill(g.getClipRect());
// }
final Rectangle frame = getBounds();
frame.x = frame.y = 0; // our GC is already offset to Component.getX/getY
frame.grow(-1, -1);
//frame.grow(-1, -4); // leave a vertical gap, and a bit of horiz room to prevent clipping at right
final Point2D.Float offset = new Point2D.Float();
final Size size = new Size(frame);
final Rectangle2D.Float allLayerBounds = new Rectangle2D.Float();
// todo: would be nice if layers cached all their children bounds
// -- LWMap should be using code for same
for (LWComponent l : layer.getMap().getChildren())
LWMap.accruePaintBounds(l.getChildren(), allLayerBounds);
final double zoom = tufts.vue.ZoomTool
.computeZoomFit(size,
0,
allLayerBounds,
offset,
0.5); // max zoom for preview is 50%
dc.g.translate(-offset.x + frame.x, -offset.y + frame.y);
dc.g.scale(zoom, zoom);
dc.setClipOptimized(false);
layer.drawZero(dc);
if (DEBUG.BOXES) {
// Would be nice if computeZoomFit could also set for us a used
// viewport size, so we wouldn't have to draw this in the scaled
// down GC, and it would be easier to create insets.
dc.setAbsoluteStroke(1);
//dc.g.setColor(Color.blue);
//dc.g.draw(allLayerBounds);
dc.g.setColor(Color.red);
//Util.grow(allLayerBounds, 5 / (float) zoom);
//Util.grow(allLayerBounds, 5);
//allLayerBounds.width -= 1/zoom;
dc.g.draw(allLayerBounds);
}
if (DEBUG.BOXES) {
final Graphics2D g = (Graphics2D) _g;
g.setColor(Color.lightGray);
Rectangle r = getBounds();
g.drawRect(0,0, r.width-1, r.height-1);
}
//g.draw(new Rectangle2D.Float(offset.x,offset.y, allLayerBounds.width, allLayerBounds.height));
//((Graphics2D)g).draw(frame);
//((LWContainer)layer).drawChildren(dc);
}
}
private static final Insets LockedInsets = new Insets(4,4,4,0);
private static final Dimension LayerHeight = new Dimension(0, 38);
private static final Dimension DefaultHeight = new Dimension(0, 28);
// private static class MouseTracker extends tufts.vue.MouseAdapter implements Runnable
// {
// boolean entered;
// Row overRow;
// MouseTracker() {
// super("layer.row.mouse-tracker");
// }
// public void mouseEntered(MouseEvent e) {
// entered = true;
// }
// public void mouseExited(MouseEvent e) {
// entered = false;
// }
// // called when mouse-entered happens on Row container
// public void setRow(Row r) {
// if (overRow != null && overRow != r)
// overRow.rollOff();
// overRow = r;
// }
// // called when mouse-exited happens on the Row container
// public void run() {
// if (!entered) {
// if (DEBUG.FOCUS) Log.debug("MOUSE-TRACKER: no child entered, rolling off for real");
// overRow.rollOff();
// } else {
// if (DEBUG.FOCUS) Log.debug("MOUSE-TRACKER: a child was entered, do not roll off row");
// }
// }
// }
/**
* Fail-safe mouse enter/exit tracker for Row's. Only "exits" a Row when a new one
* is entered, or a parent of all the Rows is exited -- otherwise, if we just rely
* on standard events, when any child of a Row is mouse-entered, the Row itself is
* exited, yet this isn't actually rolling-off the row.
*/
private static class MouseTracker extends tufts.vue.MouseAdapter implements Runnable
{
Row overRow;
MouseTracker() {
super("layer.row.mouse-tracker");
}
/** MOUSE_ENTERED events on Rows should be forwarded here */
public void recordMouseEntered(Row row, MouseEvent e) {
setRow(row);
}
public void mouseExited(MouseEvent e) {
// any potential parent of a Row, that could possibly get a mouseExited
// event, should be re-routed here. MOUSE_EXITED events can appear very
// unreliably, so as many parents as possible should be tracking for these
// events in the hope that at least one of them will get the event, even
// when the mouse is moving fast. Even then, we can still miss some. It
// appears we may even need to check for WINDOW_LOST_FOCUS events on the top
// level window for more reliability -- and we must create a listener --
// there appears to be no Window state we can reliably poll for this!
setRow(null);
}
// called when mouse-entered happens on Row container
void setRow(Row r) {
if (overRow != null && overRow != r)
overRow.rollOff();
overRow = r;
if (overRow != null)
overRow.rollOn();
}
public void run() {
// todo: only real fail-safe method of handling this will be to reset a
// timer each we get here (Row MOUSE_EXITED has happened), and then if after
// 500ms or so, another Row hasn't been entered, we can roll off the last
// row. Tho we should be able to cancel the timer right off and do nothing
// if overRow has changed since the check was scheduled in the AWT event
// queue (e.g., right after MOUSE_EXITED, a MOUSE_ENTERED happened on
// another row, so overRow is different, and until we see another Row
// MOUSE_EXITED, we don't need the failsafe timer).
// if (overRow != null) {
// Window parent = SwingUtilities.getWindowAncestor(overRow);
// if (!parent.isFocused()) {
// if (DEBUG.FOCUS) Log.debug("rolling off last row as window has lost focus");
// setRow(null);
// } else if (DEBUG.FOCUS) {
// if (DEBUG.FOCUS) Log.debug("parent still focused: " + GUI.name(parent));
// }
// }
}
}
private final MouseTracker RowMouseEnterExitTracker = new MouseTracker();
private class Row extends JPanel implements javax.swing.event.MouseInputListener, Runnable {
final AbstractButton exclusive;
final AbstractButton visible = new JCheckBox();
final AbstractButton locked = new JRadioButton();
//final JLabel activeIcon = new JLabel();
final JTextField label;
final JPanel preview;
final AbstractButton grab;
final LWComponent layer;
final Color defaultBackground;
Row(final LWComponent layer)
{
this.layer = layer;
label = new TextEdit(this);
setToolTipText(label.getText());
setName(layer.toString());
setLayout(new GridBagLayout());
setBorder(new CompoundBorder(new MatteBorder(1,0,1,0, Color.lightGray),
GUI.makeSpace(3,7,3,0)));
if (SCROLLABLE) {
if (layer instanceof Layer)
setPreferredSize(LayerHeight);
else
setPreferredSize(DefaultHeight);
}
//setMaximumSize(new Dimension(Short.MAX_VALUE, 64)); // no effect
//setMinimumSize(new Dimension(150, 100)); // no effect
addMouseListener(this);
addMouseMotionListener(this);
if (layer instanceof Layer)
defaultBackground = Color.white;// Changed for Background
else
defaultBackground = Color.white; // debug/test case
setBackground(defaultBackground);
if (true) {
// looks a bit messy w/current icons, but more informative
visible.setName("layer.visible");
visible.setIcon(VueResources.getImageIcon("pathwayOff"));
visible.setSelectedIcon(VueResources.getImageIcon("pathwayOn"));
// need a bigger and/or colored icon -- to tough to see
locked.setName("layer.locked");
locked.setIcon(VueResources.getImageIcon("lockOpen"));
locked.setSelectedIcon(VueResources.getImageIcon("lock"));
locked.setMargin(LockedInsets);
locked.setBorder(GUI.makeSpace(1,5,5,1)); // no effect
}
locked.setSelected(layer.isLocked());
locked.setBorderPainted(layer.isLocked());
locked.setOpaque(false);
locked.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
//locked.setBorderPainted(locked.isSelected());
layer.setLocked(locked.isSelected());
if (layer == getActiveLayer() && !canBeActive(layer))
if (AUTO_ADJUST_ACTIVE_LAYER) attemptAlternativeActiveLayer(false);
}});
visible.setSelected(layer.isVisible());
visible.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
layer.setVisible(visible.isSelected());
locked.setEnabled(layer.isVisible());
label.setEnabled(layer.isVisible());
VUE.getMainWindow().repaint();
if (layer == getActiveLayer() && !canBeActive(layer))
if (AUTO_ADJUST_ACTIVE_LAYER) attemptAlternativeActiveLayer(false);
layer.getUndoManager().mark((layer.isVisible() ? "Show" : "Hide")
+ " Layer");
}});
label.setEnabled(layer.isVisible());
if (layer instanceof Layer) {
exclusive = new JRadioButton();
exclusive.setName(VueResources.getString("layer.exclusive"));
exclusive.setToolTipText(VueResources.getString("layer.quickedit"));
exclusive.setBorderPainted(false);
exclusive.setIcon(VueResources.getIcon(VUE.class, "images/quickFocus_ov.png"));
exclusive.setFocusable(false);
exclusive.setOpaque(false);
exclusive.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
exclusive.setBorderPainted(exclusive.isSelected());
Row.this.setExclusive(exclusive.isSelected());
}});
//grab = new JButton("Grab");
//grab.setFont(VueConstants.SmallFont);
grab = new JRadioButton();
grab.setName(VueResources.getString("button.grab"));
grab.setToolTipText(VueResources.getString("layer.selection.tooltip"));
grab.setBorderPainted(false);
grab.setIcon(VueResources.getIcon(VUE.class, "images/grab_ov.png"));
grab.setFocusable(false); // FYI, no help on ignoring mouse-motion
grab.setOpaque(false);
// // todo: use icon-button version when ready to go -- may
// // want to use a VueButton
// grab = new JButton();
// grab.setBorderPainted(false);
// // todo: update when Melanie creates new icon for this
// grab.setIcon(VueResources.getIcon(VUE.class, "images/hand_open.png"));
// grab.putClientProperty("JButton.buttonType", "textured");
// grab.putClientProperty("JButton.sizeVariant", "tiny");
grab.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (VUE.getSelection().size() > 0) {
grabFromSelection((Layer)layer);
VUE.getUndoManager().mark("Move To Layer " + Util.quote(layer.getLabel()));
}
}});
} else {
exclusive = null;
grab = null;
}
final JLabel info = new JLabel()
//{ public Dimension getMinimumSize() { return GUI.ZeroSize; } }
;
info.setMinimumSize(new Dimension(40,30));
if (layer.supportsChildren()) {
// This might slow down undo of some large-set operations in large maps,
// such as grabs, as auto-reparenting will currently de-parent each
// child separately, issuing an event for each. (All hierarchy events,
// however, are merged into a single one for undo/redo for each parent).
// Note: depends on Layer having permitZombieEvent(e) return
// true, otherwise won't update correctly on undo.
final LWComponent.Listener countListener
= new LWComponent.Listener() {
public void LWCChanged(LWCEvent e) {
//if (DEBUG.Enabled) Log.debug("UPDATING " + Row.this + " " + e);
String counts = "";
final int nChild = layer.numChildren();
final int allChildren = layer.getDescendentCount();
info.setForeground(Color.gray);
if (nChild > 0)
counts += nChild;
if (allChildren != nChild)
counts += "/" + allChildren;
if(counts.length()==0){
info.setText("");
}else{
info.setText("("+counts+")");
}
//if (DEBUG.Enabled) { Row.this.validate(); GUI.paintNow(Row.this); } // slower
// above will usually cause a deadlock tho when dropping images and this UI is visible
//if (DEBUG.Enabled) { Row.this.validate(); GUI.paintNow(info); } // faster
}};
countListener.LWCChanged(null); // do the initial set
layer.addLWCListener(countListener, LWKey.ChildrenAdded, LWKey.ChildrenRemoved);
}
// activeIcon.setIcon(VueResources.getIcon(VUE.class, "images/hand_open.png"));
// // todo perf: only actually need instance of each of these for all rows:
// activeIcon.setDisabledIcon(new GUI.EmptyIcon(activeIcon.getIcon()));
// activeIcon.setBorder(GUI.makeSpace(4,0,0,0));
//final JComponent label = new VueTextField(layer.getLabel());
// VueTextField impl not useful to us (also not used anywhere)
// -- we need an impl that works just like VueTextPane, except
// as a single line of text.
//final JLabel info = new JLabel("(" + layer.numChildren() + " items)");
final GridBagConstraints c = new GridBagConstraints();
c.weighty = 1; // 1 has all expanding to fill vertical, 0 leaves all at min height
c.anchor = GridBagConstraints.WEST;
// c.insets.right = 4;
// add(exclusive, c);
//add(Box.createHorizontalStrut(5), c);
c.insets.right = 0;
add(visible, c);
info.setHorizontalAlignment(SwingConstants.RIGHT);
if (true) {
// this magic, setting min-size to zero on the info text to 0, and wrapping
// it in a container with the label, allows it fill left, shriking the
// edit label if need-be, but never expanding the size of the two
// components togehter -- that way, all label-edit + info-text groups
// in all rows will always have the same width, keeping everything
// in alignment
info.setMinimumSize(new Dimension(40,23));
label.setMaximumSize(new Dimension(88,23));
label.setCaretPosition(0);
//info.addMouseListener(RowMouseEnterExitTracker);
Box box = new Box(BoxLayout.X_AXIS);
//JPanel box = new JPanel();
label.setPreferredSize(null); // must remove this, or info gets squished to 0 width
//label.addMouseListener(RowMouseEnterExitTracker);
box.add(label);
//box.add(Box.createHorizontalGlue());
box.add(info);
if (DEBUG.BOXES) box.setBorder(new LineBorder(Color.red));
box.setPreferredSize(GUI.MaxSize);
//box.setPreferredSize(new Dimension(200, 30));
//box.setMaximumSize(new Dimension(200, 30)); // apparently no use
c.weightx = 1;
c.fill = GridBagConstraints.HORIZONTAL;
c.insets.right = 0;
add(box, c);
c.insets.left = 0;
//c.insets.right = 0;
c.weightx = 0;
c.fill = GridBagConstraints.NONE;
} else {
c.weightx = 1;
c.fill = GridBagConstraints.HORIZONTAL;
add(label, c);
c.fill = GridBagConstraints.NONE;
c.weightx = 0;
add(info, c);
// add(Box.createHorizontalStrut(1));
// add(label);
// //add(Box.createHorizontalGlue());
// add(Box.createHorizontalStrut(1));
// //info.setBorder(new LineBorder(Color.red));
// //info.setPreferredSize(new Dimension(70,Short.MAX_VALUE));
// //info.setMinimumSize(new Dimension(60,0));
// add(info);
}
//add(Box.createHorizontalGlue(), c);
info.setMinimumSize(new Dimension(40,23));
label.setMinimumSize(new Dimension(88,23));
if (layer.hasFlag(INTERNAL)) {
add(locked, c);
preview = null;
return;
}
preview = new Preview(layer);
//preview.setMinimumSize(new Dimension(128, 64));
//preview.setPreferredSize(GUI.MaxSize);
//preview.setSize(256,128);
//preview.setPreferredSize(new Dimension(256, Short.MAX_VALUE));
//preview.setMaximumSize(GUI.MaxSize);
if (false && DEBUG.Enabled)
layer.addLWCListener(new LWComponent.Listener() {
public void LWCChanged(LWCEvent e) {
// this is heavy duty! Would be nice if UserActionCompleted
// came through the layer, and we could listen for that,
// but it comes through the map
preview.repaint();
}});
//add(Box.createHorizontalGlue());
if (preview != null) {
c.weightx = 1;
c.fill = GridBagConstraints.BOTH;
//c.fill = GridBagConstraints.VERTICAL;
//add(Box.createHorizontalStrut(7));
add(preview, c);
c.weightx = 0;
c.fill = GridBagConstraints.NONE;
}
if (true) {
JPanel fixed = new JPanel(new BorderLayout());
fixed.setOpaque(true);
fixed.setBorder(GUI.makeSpace(3,1,3,1));
fixed.setMinimumSize(new Dimension(53, 0));
//fixed.add(exclusive, BorderLayout.WEST);
locked.setMinimumSize(new Dimension(25, 0));
if(grab!=null){
grab.setMinimumSize(new Dimension(25, 0));
fixed.add(grab, BorderLayout.CENTER);
}
fixed.add(locked, BorderLayout.EAST);
c.fill = GridBagConstraints.BOTH;
add(fixed, c);
} else {
// old-style before we added hiding these on mouse roll-off
//add(activeIcon, c);
add(grab, c);
add(locked, c);
}
// set initial visibility states by simulating a mouse roll-off
rollOff();
}
private void add(Component comp, GridBagConstraints c) {
if (comp == null)
return;
//super.add(comp);
super.add(comp, c);
//comp.addMouseListener(RowMouseEnterExitTracker);
//c.gridx++;
}
private void processEventUp(AWTEvent e) {
super.processEvent(e);
}
private void setExclusive(final boolean excluding) {
Row exclusiveRow = fetchExclusiveRow();
//Log.debug("SET-EXCLUSIVE " + this + " = " + excluding + "; nowExclusive=" + exclusiveRow);
if (excluding && exclusiveRow == this)
return;
final boolean wasExcluding = exclusiveRow != null;
final Row lastExcluded = exclusiveRow;
exclusiveRow = this;
if (wasExcluding && excluding) {
// disable old row
lastExcluded.label.setEnabled(false);
lastExcluded.layer.setHidden(LAYER_EXCLUDED);
lastExcluded.exclusive.setVisible(false); // simulate rollOff
lastExcluded.exclusive.setSelected(false);
lastExcluded.exclusive.setBorderPainted(false); // clear sticky-state border
}
if (excluding) {
if (!wasExcluding)
setExclusiveMode(true, this);
storeExclusiveRow(this);
if (layer instanceof Layer)
setActiveLayer((Layer) layer);
} else if (exclusiveRow == this) {
setExclusiveMode(false, null);
return;
}
exclusive.setSelected(excluding);
label.setEnabled(true);
layer.clearHidden(DEFAULT);
layer.clearHidden(LAYER_EXCLUDED);
layer.setLocked(false);
indicateActiveLayers(null);
//tufts.vue.ZoomTool.setZoomFit();
}
@Override
protected void addImpl(Component comp, Object constraints, int index) {
if (comp instanceof JComponent)
((JComponent)comp).setOpaque(false); // needed for Tiger (uneeded on Leopard)
comp.setFocusable(false);
//comp.setFocusable(comp instanceof JTextField);
super.addImpl(comp, constraints, index);
}
@Override
public void setBackground(Color bg) {
//Log.debug(this + " setBackground " + bg);
if (bg == null) {
super.setBackground(defaultBackground);
// if (label != null)
// label.setBackground(defaultBackground);
} else {
super.setBackground(bg);
// if (label != null) {
// if (bg.getAlpha() != 255) {
// //label.setBackground(Color.red);
// label.setBackground(new Color(0,0,0,0));
// label.setOpaque(false);
// } else {
// label.setBackground(bg);
// }
// }
}
}
private int dragStartX;
private int dragStartY;
private int dragStartMouseY;
private int dragRowIndex;
private int dragLastY;
private boolean didReorder;
private Color saveColor;
public void mouseEntered(MouseEvent e) {
RowMouseEnterExitTracker.recordMouseEntered(this, e);
}
public void mouseExited(MouseEvent e) {
// //Util.printStackTrace("HERE");
// RowMouseEnterExitTracker.setRow(this);
if (DEBUG.FOCUS) Log.debug("SCHEDULING MOUSE-TRACKER on " + e);
GUI.invokeAfterAWT(RowMouseEnterExitTracker);
}
void rollOn() {
if (grab != null) {
grab.setVisible(true);
//grab.setFocusable(false); // NO HELP ON IGNORING MOUSE-MOTION
}
if (locked != null){
//locked.setBorder(BorderFactory.createEmptyBorder(0, 35, 0, 0));
locked.setVisible(true);
}
if (exclusive != null)
exclusive.setVisible(true);
}
void rollOff() {
if (grab != null){
grab.setVisible(false);
}
if (locked != null && !locked.isSelected())
locked.setVisible(false);
if (exclusive != null && !exclusive.isSelected())
exclusive.setVisible(false);
}
public void mouseClicked(MouseEvent e) {
if (GUI.isDoubleClick(e)) {
if(e.isShiftDown()){
if (VUE.getSelection() !=null)
VUE.getSelection().add(layer.getChildren());
else
VUE.getSelection().setTo(new LWSelection(layer.getChildren()));
setActiveLayer((Layer) layer, !UPDATE);
}else{
VUE.getSelection().setTo(layer.getAllDescendents());
}
}
Layer active = null;
if(((JButton)mToolbar.getComponent(3)).isBorderPainted()){
active = getActiveLayer();
Row row =null;
for (Row rows : mRows){
row = rows;
if (rows.layer.equals(active)){
row.layer.setVisible(true);
VUE.getSelection().setTo(row.layer.getAllDescendents());
}else{
row.layer.setVisible(false);
}
}
}
if(active != null){
MouseListener popupListener = new PopupListener(active.isLocked());
addMouseListener(popupListener);
}
}
class PopupListener extends MouseAdapter {
boolean lockFlg = true;
PopupListener(boolean lockFlg){
this.lockFlg = lockFlg;
}
public void mousePressed(MouseEvent e) {
showPopup(e);
}
public void mouseReleased(MouseEvent e) {
showPopup(e);
}
private void showPopup(MouseEvent e) {
if (e.isPopupTrigger()) {
if(lockFlg){
unlockPopupMenu.show(e.getComponent(), e.getX(), e.getY());
}else{
popupMenu.show(e.getComponent(), e.getX(), e.getY());
}
}
}
private void setPopEnabled(boolean isFlg){
duplicateMenuItem.setEnabled(isFlg);
renameMenuItem.setEnabled(isFlg);
lockMenuItem.setEnabled(isFlg);
deleteMenuItem.setEnabled(isFlg);
if(mRows.size() == 1){
deleteMenuItem.setEnabled(false) ;
}
}
}
public void mousePressed(MouseEvent e) {
if (DEBUG.MOUSE) Log.debug(e);
if (layer instanceof Layer) {
// // if (e.isShiftDown())
// // mSelection.toggle(layer);
// // else
// mSelection.setTo(layer);
// if (inExclusiveMode()) // exlusive mode is no longer globally modal
// setExclusive(true);
// else
if (!AUTO_ADJUST_ACTIVE_LAYER || layer.isVisible())
setActiveLayer((Layer) layer, UPDATE);
// if (VUE.getSelection().isEmpty() || VUE.getSelection().only() instanceof Layer)
// VUE.getSelection().setTo(layer);
} else {
// this case for debug/test only: we shouldn't normally
// see regular objects a the top level of the map anymore
VUE.getSelection().setTo(layer);
}
//LayersUI.this.requestFocus();
isDragUnderway = false;
mDragRow = null;
didReorder = false;
}
public void mouseReleased(MouseEvent e) {
isDragUnderway = false;
mDragRow = null;
if (saveColor != null) {
setBackground(saveColor);
saveColor = null;
}
layoutRows();
if (didReorder) {
didReorder = false;
if (!DYNAMIC_UPDATE) {
Util.printStackTrace("unimplemented");
// a implement LWContainer.insertAt(index, LWComponent) (can just use mChildren.add(index, c)
return;
}
if (dragRowIndex == mRows.indexOf(this)) {
layer.getMap().getUndoManager().resetMark();
} else {
String undoMsg;
if (dragRowIndex > mRows.indexOf(this))
undoMsg = "Raise Layer ";
else
undoMsg = "Lower Layer ";
layer.getMap().getUndoManager().mark(undoMsg + Util.quote(layer.getLabel()));
}
}
}
public void mouseMoved(MouseEvent e){}
private int getDropRegionSize() {
if (mRows.size() > 7) {
// by using less than total height, we leave a narrow visible region
// where the original item was as a reminder to the user of where
// they're dragging from
return (int) (0.8f * getHeight());
} else
return getHeight();
}
private static final boolean DYNAMIC_UPDATE = true;
public void mouseDragged(MouseEvent e)
{
final Point mouse = SwingUtilities.convertPoint(this, e.getPoint(), getParent());
if (!isDragUnderway) {
//System.out.println("START-DRAG");
dragStartX = getX();
dragStartY = getY();
dragStartMouseY = mouse.y;
dragLastY = mouse.y;
dragRowIndex = mRows.indexOf(this);
getParent().setComponentZOrder(this, 0); // make sure always paints on top
isDragUnderway = true;
mDragRow = this;
Color c = getBackground();
setBackground(new Color(c.getRed(),c.getGreen(),c.getBlue(),128));
saveColor = c;
didReorder = false;
}
int newY = dragStartY + (mouse.y - dragStartMouseY);
final int dy = newY - dragStartY;
if (newY < 0)
newY = 0;
else if (newY > getParent().getHeight() - getHeight())
newY = getParent().getHeight() - getHeight();
//System.out.println("newY=" + newY + "; dy=" + dy);
boolean moved = false;
final int curIndex = mRows.indexOf(this);
if (mouse.y < dragLastY && curIndex > 0) {
final Row above = mRows.get(curIndex - 1);
if (newY < above.getY() + above.getHeight() / 2) {
if (DYNAMIC_UPDATE) {
// will trigger reload/relayout of all rows
moved = layer.getParent().bringForward(layer);
} else {
if (DEBUG.Enabled) Log.debug("BUMP DOWN " + above);
above.setLocation(above.getX(), above.getY() + getDropRegionSize());
Collections.swap(mRows, curIndex, curIndex - 1);
moved = true;
}
}
} else if (mouse.y > dragLastY && curIndex < mRows.size() - 1) {
final Row below = mRows.get(curIndex + 1);
final int bottomEdge = newY + getHeight();
if (bottomEdge > below.getY() + below.getHeight() / 2) {
if (DYNAMIC_UPDATE) {
// will trigger reload/relayout of all rows
moved = layer.getParent().sendBackward(layer);
} else {
if (DEBUG.Enabled) Log.debug("BUMP UP " + below);
below.setLocation(below.getX(), below.getY() - getDropRegionSize());
Collections.swap(mRows, curIndex, curIndex + 1);
moved = true;
}
}
}
if (moved) {
loadLayers(mMap);
getParent().setComponentZOrder(this, 0); // make sure always paints on top
didReorder = true;
}
setDragLocation(getX(), newY);
dragLastY = mouse.y;
}
@Override
public void setBounds(int x, int y, int width, int height) {
// This prevents us from being laid-out to the new location
// during drags (jumping to the new location for one mouse-move)
// Must be paired with setDragLocation to work.
if (mDragRow == this)
return;
else
super.setBounds(x, y, width, height);
}
private void setDragLocation(int x, int y) {
super.setBounds(x, y, getWidth(), getHeight());
}
public void run() {
setVisible(true);
}
@Override
public String toString() {
return "Row[" + mRows.indexOf(this) + "; " + layer + "]";
}
}
public class SubtleSquareBorder implements Border
{
protected int m_w = 6;
protected int m_h = 6;
protected Color m_topColor = Color.gray;
protected Color m_bottomColor = Color.gray;
protected boolean roundc = false; // Do we want rounded corners on the border?
public SubtleSquareBorder(boolean round_corners)
{
roundc = round_corners;
}
public Insets getBorderInsets(Component c)
{
return new Insets(m_h, m_w, m_h, m_w);
}
public boolean isBorderOpaque()
{
return true;
}
public void paintBorder(Component c, Graphics g, int xx, int yy, int ww, int hh)
{
int x = xx + 5;
int y = yy + 5;
int w = ww - 12;
int h = hh-12;
//w = w - 3;
//h = h - 3;
x ++;
y ++;
// Rounded corners
if(roundc)
{
g.setColor(m_topColor);
g.drawLine(x, y + 3, x, y + h - 2);
g.drawLine(x + 2, y, x + w - 2, y);
g.drawLine(x, y + 2, x + 2, y); // Top left diagonal
g.drawLine(x, y + h - 2, x + 2, y + h); // Bottom left diagonal
g.setColor(m_bottomColor);
g.drawLine(x + w, y + 2, x + w, y + h - 2);
g.drawLine(x + 2, y + h, x + w -2, y + h);
g.drawLine(x + w - 2, y, x + w, y + 2); // Top right diagonal
g.drawLine(x + w, y + h - 2, x + w -2, y + h); // Bottom right diagonal
}
// Square corners
else
{
g.setColor(m_topColor);
g.drawLine(x, y, x, y + h);
g.drawLine(x, y, x + w, y);
g.setColor(m_bottomColor);
g.drawLine(x + w, y, x + w, y + h);
g.drawLine(x, y + h, x + w, y + h);
}
}
}
}
// addKeyListener(new KeyAdapter() {
// public void keyPressed(KeyEvent e) {
// // Why aren't the mark's working?
// //System.out.println("KP " + e);
// final tufts.vue.LWContainer layer = VUE.getActiveLayer();
// if (layer == null)
// return;
// if (e.getKeyCode() == KeyEvent.VK_UP) {
// if (layer.getParent().bringForward(layer)) {
// layer.getMap().getUndoManager().mark("Raise Layer " + Util.quote(layer.getLabel()));
// }
// } else if (e.getKeyCode() == KeyEvent.VK_DOWN) {
// if (layer.getParent().sendBackward(layer)) {
// layer.getMap().getUndoManager().mark("Lower Layer " + Util.quote(layer.getLabel()));
// }
// }
// }
// });
// private static final String LAYER_NEW = "New";
// private static final String LAYER_DUPLICATE = "Duplicate";
// private static final String LAYER_MERGE = "Merge";
// private static final String LAYER_DELETE = "Delete";
// public void actionPerformed(ActionEvent e) {
// if (mMap == null)
// return;
// final String a = e.getActionCommand();
// final Layer active = mMap.getActiveLayer();
// if (a == LAYER_NEW) {
// mMap.addChild(new LWMap.Layer());
// } else if (a == LAYER_DUPLICATE) {
// Layer dupe = (Layer) active.duplicate();
// mMap.addChild(dupe);
// } else if (a == LAYER_MERGE) {
// } else if (a == LAYER_DELETE) {
// mMap.deleteChildPermanently(active);
// } else {
// Log.warn("unhandled action: " + e);
// }
// mMap.getUndoManager().mark(a + " Layer");
// }