/*
* 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 edu.tufts.vue.metadata.ui;
import java.util.*;
import java.awt.BorderLayout;
import java.awt.Point;
import java.awt.Color;
import java.awt.Insets;
import java.awt.event.*;
import javax.swing.BorderFactory;
import javax.swing.CellEditor;
import javax.swing.DefaultCellEditor;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.ListModel;
import javax.swing.ComboBoxModel;
import javax.swing.border.Border;
import javax.swing.table.*;
import tufts.Util;
import tufts.vue.DEBUG;
import tufts.vue.ActiveEvent;
import tufts.vue.ActiveListener;
import tufts.vue.LWComponent;
import tufts.vue.LWGroup;
import tufts.vue.LWSelection;
import tufts.vue.VUE;
import tufts.vue.VueResources;
import tufts.vue.gui.GUI;
import edu.tufts.vue.metadata.CategoryModel;
import edu.tufts.vue.metadata.MetadataList;
import edu.tufts.vue.metadata.VueMetadataElement;
import edu.tufts.vue.ontology.OntType;
import edu.tufts.vue.rdf.RDFIndex;
/*
* MetadataEditor.java
*
* The Metadata Editor is primarily a keyword(/category) editor
* in VUE 2.0.
*
* It has the potential to display VueMetadataElements
* of non category type and was developed in initial demos
* to be more flexible in this regard. If neccesary, it shouldn't
* be too difficult to recover such flexibility.
*
* See OntologicalMembershipPane for an example of
* an editor for other types of VueMetadataElement data
*
* A slightly thornier issue has arisen in terms of applying
* keywords to multiple selections as this component now
* listens to both the current Active and current Selection so
* that it can display/edit either the current Component's
* MetadataList or that of an LWGroup - additionally
* the behavior that propagates the editing/merging of data into the sub
* components of the group currently resides here - at some
* point it might make sense to factor this behavior into
* a subclass, particularly if needed elsewhere.
*
*
* Created on June 29, 2007, 2:37 PM
*
* @author dhelle01
*/
public class MetadataEditor extends JPanel
implements ActiveListener, MetadataList.MetadataListListener, LWSelection.Listener
{
private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(MetadataEditor.class);
private static final boolean AUTO_ADD_EMPTY = false; // leaving this on for now, tho we should refactor not to need it
private static final boolean DEBUG_LOCAL = DEBUG.PAIN;
//public final static String CC_ADD_LOCATION_MESSAGE = "<html>Click ⊕" // nice idea, but ⊕ looks like crap unless it's huge
public final static String CC_ADD_LOCATION_MESSAGE = "<html>Click + to permanently add this<br>custom category to your own list.";
//public final static int ROW_HEIGHT = 31;
public final static int ROW_HEIGHT = 27;
public final static int CELL_VERT_INSET = 4;
public final static int CELL_HORZ_INSET = 6; // Left aligns with JTextField editor text in Mac Aqua -- todo: check Windows
public final static int BUTTON_COL_WIDTH = 27;
private static final java.awt.Color GRID_COLOR = (DEBUG.BOXES) ? Color.black : new Color(196,196,196);
public final static Object CC_ADD_LEFT = "left-edge";
public final static Object CC_ADD_RIGHT = "right-edge";
public final static Border insetBorder = GUI.makeSpace(CELL_VERT_INSET, CELL_HORZ_INSET, CELL_VERT_INSET, CELL_HORZ_INSET/2);
public final static Border buttonBorder = GUI.makeSpace(CELL_VERT_INSET, CELL_HORZ_INSET-1, CELL_VERT_INSET, CELL_HORZ_INSET-1);
public final static Border insetBorderNoBottom = GUI.makeSpace(CELL_VERT_INSET, CELL_HORZ_INSET, CELL_VERT_INSET-1, CELL_HORZ_INSET);
public final static Border fullBox = BorderFactory.createMatteBorder(1,1,1,1,GRID_COLOR);
// public final static Border verticalStartingBox = BorderFactory.createMatteBorder(0,1,1,1,GRID_COLOR);
public final static Border noBottomBox = BorderFactory.createMatteBorder(1,1,0,1,GRID_COLOR);
public final static Border topLeftBotBox = BorderFactory.createMatteBorder(1,1,1,0,GRID_COLOR);
public final static Border topLeftBox = BorderFactory.createMatteBorder(1,1,0,0,GRID_COLOR);
public final static Border oneLeftBox = GUI.makeSpace(0,1,0,0);
private final static Border DebugBorder = BorderFactory.createMatteBorder(1,1,1,1,Color.red);
private final MDTable mdTable;
private final MDTableModel model;
private CellEditor activeTextEdit;
private int activeEditRow = -1;
private int activeEditCol = -1;
/** table-column: these are convenience comparitors: they will change when the structure changes (BC_BUTTON should always == buttonColumn) */
private int TC_BUTTON, TC_TEXT, TC_COMBO;
private static boolean DisplayChangesDuringEdit = true;
/**
* If the selection was of multiple objects, it's put in this group, otherwise this is null. This serves
* TWO purposes. The main one is that we now have a temporary internal LWComponent with a MetadataList we
* can work with. The second is simply so we can take advantage of calling getAllDescendents(), if that's
* what we wish. All we REALLY need tho is a list of VME's, as they're cached as being category only
* anyway. In fact, maybe we should make THAT the common model object: a VME list cache.
*/
private LWGroup group;
private final Set<LWComponent> groupContents = new HashSet<LWComponent>();
/** if selection was size 1, this is set to selection.first(), otherwise, null */
private LWComponent single;
// We could add the analysis code for these as a feature of LWSelection:
private final Set<String> commonKeys = new HashSet<String>();
private final List<VueMetadataElement> commonPairs = new ArrayList<VueMetadataElement>();
/** the column the +/- buttons are at: changes table structure changes to display categories combo box
* JTextField column is tested as: buttonColumn-1, JComboBox column as: buttonColumn-2 */
private int buttonColumn = 1;
private final JButton autoTagButton = new JButton(VueResources.local("keywordPanel.autotag"));
private static final CategoryModel OntologiesList = VUE.getCategoryModel();
private static final CategoryComboBoxModel CategoryMenuModel = new CategoryComboBoxModel();
/** @see ensureModelBulge() */
private int PlusOneWhenAdding = 0;
private static final String EmptyValue = "";
private static final String[] DefaultVMEObject = new String[] { VueMetadataElement.ONTOLOGY_NONE, EmptyValue };
/** for newly added items, not existing items */
private static final VueMetadataElement InputVME = new VueMetadataElement(-1, DefaultVMEObject[0], DefaultVMEObject[1], DefaultVMEObject.clone()) {
@Override public void setType(int t) { throw new Error("setType " + t); }
@Override public void setKey(String k) { throw new Error("setKey " + k); }
@Override public void setValue(String v) { throw new Error("setValue " + v); }
@Override public void setObject(Object o) { throw new Error("setObject " + Util.tags(o)); }
};
private static final java.util.EventObject EDIT_REQUEST = new java.util.EventObject("<md-edit-request>");
private final class MDTable extends JTable {
MDTable(AbstractTableModel m) {
super(m);
setName("ME:JTable");
// It's crucial that the table itself not be able to take the focus, otherwise it
// sometimes will steal it from the cell editor, which then won't get a focus border,
// and will not issue the crucial focusLost event.
setFocusable(false);
setShowGrid(true);
setGridColor(GRID_COLOR);
//setIntercellSpacing(new java.awt.Dimension(0,0));
// None of these below seem to be making any difference in terms of the BasicTableUI
// calling down through:
// at edu.tufts.vue.metadata.ui.MetadataEditor$MDCellEditor.isCellEditable(MetadataEditor.java:1503)
// at javax.swing.JTable.editCellAt(JTable.java:3482)
// at javax.swing.plaf.basic.BasicTableUI$Handler.adjustSelection(BasicTableUI.java:1078)
setRowSelectionAllowed(false); // MetaButton uses/used this to detect row
setColumnSelectionAllowed(false);
setCellSelectionEnabled(false); // note: defaults true if both row & col selection enabled
setDefaultRenderer(Object.class, new MDCellRenderer());
setDefaultEditor(JComboBox.class, new ComboCE());
setDefaultEditor(JTextField.class, new TextCE());
setDefaultEditor(Object.class, new ButtonCE());
((DefaultCellEditor)getDefaultEditor(java.lang.Object.class)).setClickCountToStart(2);
setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
setRowHeight(ROW_HEIGHT);
getTableHeader().setReorderingAllowed(false);
}
public void repaintLater() {
GUI.invokeAfterAWT(new Runnable() { public void run() { repaint(); }});
}
@Override public String getToolTipText(MouseEvent me) {
if (getEventIsOverAddLocation(me))
return CC_ADD_LOCATION_MESSAGE;
else
return null;
// final int row = rowAtPoint(me.getPoint());
// final int col = columnAtPoint(me.getPoint());
// try {
// return model.getValueAt(row, col).getKey();
// } catch (Exception e) {
// if (DEBUG.Enabled) Log.debug("tooltip: " + e);
// }
// return null;
//return "Row: " + row + " Col: " + col;
}
// @Override public boolean getRowSelectionAllowed() { return false; }
// @Override public boolean getColumnSelectionAllowed() { return false; }
// @Override public boolean getCellSelectionEnabled() { return false; }
public void stopAnyEditing() {
if (isEditing())
getCellEditor().stopCellEditing();
}
public void cancelAnyEditing() {
if (isEditing())
getCellEditor().cancelCellEditing();
}
}
private static String shortKey(String k) {
if (k == null) return DEBUG.Enabled ? "null" : "";
final int localPart = k.indexOf('#'); // RDFIndex.ONT_SEPARATOR_CHAR
if (localPart > 1 && localPart < (k.length() - 1))
return k.substring(localPart + 1);
else
return k;
}
public MetadataEditor(tufts.vue.LWComponent oneOff, boolean showOntologicalMembership, boolean followAllActive)
{
//VUE-846 -- special case related to VUE-845 (see comment on opening from keyword item in menu)
if (getSize().width < 100)
setSize(new java.awt.Dimension(300,200));
autoTagButton.setAction(tufts.vue.AnalyzerAction.calaisAutoTagger);
// autoTagButton.setLabel(VueResources.getString("keywordPanel.autotag"));
autoTagButton.setFont(tufts.vue.gui.GUI.SmallFont);
this.single = oneOff; // rare usage
mdTable = new MDTable(this.model = new MDTableModel());
mdTable.setBackground(getBackground());
mdTable.getTableHeader().addMouseListener(new java.awt.event.MouseAdapter() {
public void mouseReleased(java.awt.event.MouseEvent evt) {
java.awt.event.MouseEvent me = javax.swing.SwingUtilities.convertMouseEvent(mdTable.getTableHeader(), evt, headerAddButtonPanel);
//javax.swing.SwingUtilities.convertMouseEvent(mdTable.getTableHeader(), evt, headerAddButton);
me.translatePoint(evt.getX()-me.getX() - mdTable.getWidth() + BUTTON_COL_WIDTH, evt.getY()-me.getY());
headerAddButtonPanel.dispatchEvent(me);
}
public void mousePressed(java.awt.event.MouseEvent evt)
{
if (DEBUG.PAIN) { VUE.diagPush("MP"); Log.debug("header mousePressed..."); }
//tufts.vue.gui.VueButton addButton = MetadataTableHeaderRenderer.getButton();
java.awt.event.MouseEvent me = javax.swing.SwingUtilities.convertMouseEvent(mdTable.getTableHeader(), evt, headerAddButtonPanel);
//javax.swing.SwingUtilities.convertMouseEvent(mdTable.getTableHeader(), evt, headerAddButton);
me.translatePoint(evt.getX()-me.getX() - mdTable.getWidth() + BUTTON_COL_WIDTH, evt.getY()-me.getY());
//headerAddButton.dispatchEvent(me);
//if(DEBUG_LOCAL) {
// System.out.println("MetadataEditor -- header -- unconverted mouse event: " + evt);
// System.out.println("MetadataEditor: converted mouse event: " + me); }
/*if(evt.getX()>mdTable.getWidth()-BUTTON_COL_WIDTH) { addNewRow(); }*/
// no need for this else, should make more sense to save whenever
// adding a new row...
//else {
final int row = mdTable.rowAtPoint(evt.getPoint());
mdTable.stopAnyEditing();
headerAddButtonPanel.dispatchEvent(me);
mdTable.repaint();
if (DEBUG.PAIN) { Log.debug("header mousePressed complete."); VUE.diagPop(); }
}
});
// addMouseListener(new tufts.vue.MouseAdapter("JPanel=MetadataEditor") { public void mousePressed(MouseEvent e) {
// int row = mdTable.rowAtPoint(e.getPoint());
// int lsr = model.lastSavedRow;
// if(lsr != row) {
// model.setSaved(lsr,true);
// model.lastSavedRow = -1;
// }
// model.refresh();
// mdTable.stopAnyEditing();
// mdTable.repaint();
// }});
mdTable.addMouseListener(new tufts.vue.MouseAdapter("MetadataEditor:JTable") {
// public void mousePressed(MouseEvent e) {
// if(model.lastSavedRow != mdTable.rowAtPoint(e.getPoint()))
// model.setSaved(model.lastSavedRow,true);
// mdTable.repaint();
// }
// /*if(evt.getX()>mdTable.getWidth()-BUTTON_COL_WIDTH) {
// java.util.List<VueMetadataElement> metadataList = MetadataEditor.this.current.getMetadataList().getMetadata();
// int selectedRow = mdTable.getSelectedRow();
// // VUE-912, stop short-circuiting on row 0, delete button has also been returned
// if( mdTable.getSelectedColumn()==buttonColumn && metadataList.size() > selectedRow) {
// metadataList.remove(selectedRow);
// mdTable.repaint();
// requestFocusInWindow(); }} */
public void mouseReleased(MouseEvent e)
{
if (getEventIsOverAddLocation(e)) {
final int row = mdTable.rowAtPoint(e.getPoint());//evt.getY()/mdTable.getRowHeight();
final VueMetadataElement vme = model.getValueAt(row, -1);
if (vme.getKey() == null) throw new Error("bad vme: " + vme);
final OntType existing = OntologiesList.getCustomCategoryByLabel(shortKey(vme.getKey()));
final OntType ontType;
if (existing == null) {
ontType = OntologiesList.addCustomCategory(shortKey(vme.getKey()));
if (DEBUG.Enabled) Log.debug("added custom cat: " + Util.tags(ontType));
} else {
ontType = existing;
if (DEBUG.Enabled) Log.debug("found custom cat: " + Util.tags(ontType));
}
final VueMetadataElement customCategory_VME = freshVME(ontType.getAsKey(), vme.getValue());
if (DEBUG.PAIN) Log.debug("updated VME with new key: " + customCategory_VME);
publishEdit(row, customCategory_VME, "Custom Category");
mdTable.stopAnyEditing();
GUI.invokeAfterAWT(new Runnable() { public void run() {
refreshAll();
if (existing == null)
OntologiesList.saveCustomOntology();
}});
}
}
});
adjustColumnModel();
//setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
setLayout(new BorderLayout());
final JPanel metaPanel = new JPanel(new BorderLayout());
final JPanel tablePanel = new JPanel(new BorderLayout());
//re-enable for VUE-953 (revert back to categories/keywords)
//tablePanel.add(mdTable.getTableHeader(), BorderLayout.NORTH);
tablePanel.add(mdTable);
tablePanel.setBorder(topLeftBox);
if (DEBUG.BOXES) {
metaPanel.setOpaque(true);
metaPanel.setBackground(Color.green);
mdTable.getTableHeader().setOpaque(true);
mdTable.getTableHeader().setBackground(Color.blue);
//mdTable.getTableHeader().setBorder(GUI.makeSpace(0,0,0,1));
} else
mdTable.getTableHeader().setOpaque(false);
metaPanel.add(mdTable.getTableHeader(), BorderLayout.NORTH);
metaPanel.add(tablePanel);
JPanel optionsPanel = new JPanel();
optionsPanel.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.LEFT));
//back to "assign categories" as per VUE-953
//final JLabel optionsLabel = new JLabel("Use full metadata schema");
final JLabel optionsLabel = new JLabel(VueResources.getString("jlabel.assigncategory"));
optionsLabel.setFont(GUI.SmallFont);
//final JButton advancedSearch = new JButton(new ImageIcon(VueResources.getURL("advancedSearchMore.raw")));//tufts.vue.gui.VueButton("advancedSearchMore");
final JCheckBox advancedSearch = new JCheckBox();
//advancedSearch.setBorder(BorderFactory.createEmptyBorder(0,0,15,0));
advancedSearch.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent e) {
//advancedSearch.setIcon(new ImageIcon(VueResources.getURL("advancedSearchLess.raw"))); // getURL("advancedSearchMore.raw")));
model.toggleCategoryKeyColumn();
advancedSearch.setSelected(model.getColumnCount() == 3);
adjustColumnModel();
revalidate();
}});
optionsPanel.add(advancedSearch);
advancedSearch.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
optionsPanel.add(optionsLabel);
optionsPanel.add(autoTagButton);
final JPanel controlPanel = new JPanel(new BorderLayout());
controlPanel.add(optionsPanel);
metaPanel.add(controlPanel, BorderLayout.SOUTH);
add(metaPanel, BorderLayout.NORTH);
// followAllActive needed for MapInspector, it only follows the map,
// not any particular component
if(followAllActive)
{
tufts.vue.VUE.addActiveListener(tufts.vue.LWComponent.class,this);
VUE.getSelection().addListener(this);
}
else
{
tufts.vue.VUE.addActiveListener(tufts.vue.LWMap.class,this);
}
MetadataList.addListener(this);
headerAddButtonPanel.addMouseListener(new java.awt.event.MouseAdapter() {
private boolean dispatchedInButton(MouseEvent e) {
//if(DEBUG_LOCAL) {
// System.out.println("MetadataEditor -- table header mouse pressed " + // e);
// System.out.println(" components count " + headerAddButtonPanel.getComponentCount());
// if(headerAddButtonPanel.getComponentCount() > 0) {
// System.out.println(" component 0 " + headerAddButtonPanel.getComponents()[0].getClass());
// System.out.println(" component 0 - location: " + headerAddButtonPanel.getComponents()[0].getLocation());
// System.out.println(" component 0 - location: " + headerAddButtonPanel.getComponents()[0].getSize());
// System.out.println(" component at location " + headerAddButtonPanel.getComponentAt(e.getPoint())); }}
final tufts.vue.gui.VueButton button = (tufts.vue.gui.VueButton) headerAddButtonPanel.getComponents()[0];
final java.awt.event.MouseEvent me = javax.swing.SwingUtilities.convertMouseEvent(headerAddButtonPanel, e, button);
// javax.swing.SwingUtilities.convertMouseEvent(mdTable.getTableHeader(), evt, headerAddButton);
me.translatePoint(-me.getX(), -me.getY());
final java.awt.Point p = e.getPoint();
final boolean inside =
p.x > button.getLocation().getX() &&
p.x < button.getLocation().getX() + button.getWidth() &&
p.y > button.getLocation().getY() &&
p.y < button.getLocation().getY() + button.getHeight();
if (inside) {
if (DEBUG.PAIN) Log.debug("header caught button hit on " + button);
// we only need to actually dispatch this if the headerAddButton below
// makes itself a MouseListener
button.dispatchEvent(me);
}
return inside;
}
public void mousePressed(MouseEvent e) {
if (dispatchedInButton(e)) {
model.ensureBulge("gui-add");
mdTable.editCellAt(model.getRowCount() - 1, TC_TEXT, EDIT_REQUEST);
mdTable.repaint();
}
}
});
/*headerAddButton.addActionListener(new java.awt.event.ActionListener(){
public void actionPerformed(java.awt.event.ActionEvent e) { if(DEBUG_LOCAL) { System.out.println("MetadataEditor -- table header add button pressed " + e); } } });*/
// Not sure what this was for: if we bother to dispatch the event to the button above, this WOULD
// let the the button display a pressed state, but it isn't configured with one, nor configured to
// try and generate them. */
headerAddButton.addMouseListener(new tufts.vue.MouseAdapter("<headerAddButton>") { public void mousePressed(MouseEvent e) {
super.mousePressed(e);
//if(DEBUG_LOCAL) System.out.println("MetadataEditor -- table header mouse pressed on button " + e);
headerAddButtonPanel.repaint();
headerAddButton.repaint();
}});
setBorder(BorderFactory.createEmptyBorder(10,8,15,6));
validate();
// As this can be lazily constructed, included the request for a selection listener, if this is the
// first time we've been created, simulate an update call from the selection.
GUI.invokeAfterAWT(new Runnable() { public void run() {
selectionChanged(VUE.getSelection());
}});
}
// @Override public Dimension getMinimumSize() {
// int height = 120;
// //int lines = 1;
// int rowCount = mdTable.getRowCount();
// int rowHeight = mdTable.getRowHeight();
// //System.out.println("rowHeight :" + rowHeight);
// return new Dimension(300,(height+((rowCount-1) * rowHeight)));
// }
// @Override public Dimension getPreferredSize() { return getMinimumSize(); }
public void refreshAll() {
model.refresh();
ignoreCategorySelectEvents = true;
try {
CategoryMenuModel.refreshCategoryMenu();
} catch (Throwable t) {
Log.error("refresh: " + CategoryMenuModel, t);
} finally {
ignoreCategorySelectEvents = false;
}
validate();
repaint();
}
public JTable getTable() { return mdTable; }
public MDTableModel getModel() { return model; }
public CellEditor getActiveTextEdit() { return activeTextEdit; }
private static VueMetadataElement emptyVME() { return freshVME(EmptyValue); }
private static VueMetadataElement freshVME(String textValue) {
return freshVME(VueMetadataElement.ONTOLOGY_NONE, textValue);
}
private static VueMetadataElement freshVME(String key, String text) {
return new VueMetadataElement(key, text); // defaults category type, inits object
// final VueMetadataElement vme = new VueMetadataElement();
// vme.setObject(new String[] { key, textValue }); // note: will force category type anyway
// vme.setType(VueMetadataElement.CATEGORY);
// return vme;
}
public boolean getEventIsOverAddLocation(MouseEvent evt)
{
final boolean locationIsOver =
evt.getX() > getCustomCategoryAddLocation(CC_ADD_LEFT) &&
evt.getX() < getCustomCategoryAddLocation(CC_ADD_RIGHT);
final int row = mdTable.rowAtPoint(evt.getPoint());//evt.getY()/mdTable.getRowHeight();
return locationIsOver && !model.wasCategoryFound(row);
}
/** returns -1 if add widget is not available (i.e. if in basic
* mode and/or not in "assign categories" mode) **/
public int getCustomCategoryAddLocation(Object ccLocationConstant)
{
if(mdTable.getModel().getColumnCount() == 2)
return -1;
if (ccLocationConstant == CC_ADD_LEFT)
return mdTable.getColumnModel().getColumn(0).getWidth() - 20;
else if(ccLocationConstant == CC_ADD_RIGHT)
return mdTable.getColumnModel().getColumn(0).getWidth();
else
return -1;
}
public void adjustColumnModel()
{
final int editorWidth = MetadataEditor.this.getWidth();
//if(MetadataEditor.this.getTopLevelAncestor() != null)
// editorWidth = MetadataEditor.this.getTopLevelAncestor().getWidth();
if (mdTable == null)
return;
if (mdTable.getModel().getColumnCount() == 2) {
mdTable.getColumnModel().getColumn(0).setHeaderRenderer(HeaderRenderer);
mdTable.getColumnModel().getColumn(1).setHeaderRenderer(HeaderRenderer);
mdTable.getColumnModel().getColumn(0).setWidth(editorWidth-BUTTON_COL_WIDTH);
mdTable.getColumnModel().getColumn(1).setMaxWidth(BUTTON_COL_WIDTH);
mdTable.getColumnModel().getColumn(1).setMinWidth(BUTTON_COL_WIDTH);
} else {
mdTable.getColumnModel().getColumn(0).setHeaderRenderer(HeaderRenderer);
mdTable.getColumnModel().getColumn(1).setHeaderRenderer(HeaderRenderer);
mdTable.getColumnModel().getColumn(2).setHeaderRenderer(HeaderRenderer);
mdTable.getColumnModel().getColumn(0).setWidth(editorWidth/2-BUTTON_COL_WIDTH/2);
mdTable.getColumnModel().getColumn(1).setWidth(editorWidth/2-BUTTON_COL_WIDTH/2);
mdTable.getColumnModel().getColumn(2).setMaxWidth(BUTTON_COL_WIDTH);
mdTable.getColumnModel().getColumn(2).setMinWidth(BUTTON_COL_WIDTH);
}
}
// Below code was to support construct such as:
// final String categoryKey = getObject(source_VME)[0];
// Using vme.getObject() v.s. just vme.getKey() might have been attempt at one point to support OntType vme.obj,
// but in any case its a horrible hack.
// private String[] getObject(VueMetadataElement vme) {
// if (vme == null) {
// if (true||DEBUG.Enabled) Log.info("getObject GIVEN NULL VME, RETURNING DEFAULT_VME_OBJECT " + Util.tags(DefaultVMEObject));
// return DefaultVMEObject;
// } else
// return (String[]) vme.getObject();
// //return vme == null ? DefaultVMEObject : (String[]) vme.getObject();
// }
// private static final String[] NullPair = { null, null };
// private static final String[] EmptyPair = { DefaultVMEObject[0], null };
// private String[] getObjectSingle(int row) {
// final int size = single.getMetadataList().getCategoryListSize();
// if (row >= size) {
// if (PlusOneWhenAdding == 1 && row == size) {
// return DefaultVMEObject;
// } else {
// Log.warn("getObjectCurrent for row " + row + " returns NullPair");
// return NullPair; // if we return EmptyPair, we'll get auto-add empty none keys again
// }
// } else
// return getObject(single.getMetadataList().getCategoryList().get(row)); // don't we have this cached?
// }
// private String[] getObjectMulti(int row) {
// return getObject(group.getMetadataList().getCategoryList().get(row));
// }
// private String[] getObjectActive(int row) {
// if (single != null)
// return getObjectSingle(row);
// else if (group != null)
// return getObjectMulti(row);
// else {
// Log.warn("no active object at row " + row);
// return NullPair;
// }
// }
// /** @return the category key at the given row */
// String getKeyForRow(int row) { return getObjectActive(row)[0]; }
// //String getValueForRow(int row) { return getObjectActive(row)[1]; }
public void selectionChanged(final LWSelection s)
{
if (DEBUG.PAIN) Log.debug("selectionChanged: " + s);
this.single = null;
this.group = null;
this.groupContents.clear();
//mdTable.stopAnyEditing(); // will auto-save: didn't used to work in selection-change case, but is now an option
mdTable.cancelAnyEditing();
autoTagButton.setVisible(s.size() == 1);
if (s.size() > 1)
loadMultiSelection(s);
else
loadSingleSelection(s.first()); // 0 or 1 elements in selection
// todo: model.loadSelection(s);
}
private void loadSingleSelection(LWComponent selected)
{
if (DEBUG.PAIN) Log.debug("loadSingleSelection: " + selected);
if (selected == null)
return;
this.single = selected;
final boolean changed;
if (single.getMetadataList().getCategoryListSize() == 0)
changed = model.ensureBulge("selected-has-no-meta");
else
changed = model.clearBulge("selected: reset for new md");
if (!changed)
model.refresh();
}
private void loadMultiSelection(final LWSelection selection)
{
if (selection.size() < 2) throw new Error("size<2="+selection.size());
this.group = LWGroup.createTemporary(selection);
// SMF: iterate getChildren: this will tag just the top-level actually selected items
// SMF: iterate getAllDescendents: the above PLUS their children, grandchildren, etc.
// SMF: Using getAllDescendents is the historical default, tho that means we can NEVER just group-tag a set of parents,
// If we only did the direct children, what's in the selection could still be expanded to include children,
// tho we don't have an action for that -- the user would have to add the children manually.
for (LWComponent c : group.getAllDescendents())
if (!c.hasFlag(LWComponent.Flag.ICON))
groupContents.add(c);
// note: a HashSet does not help us much here, except perhaps for the remove(o) calls in AbstractCollection.retainAll
final List<VueMetadataElement> shared = new ArrayList<VueMetadataElement>();
// This finds the intersection for exact key+value pairs, so it's very easy for this to display
// nothing in common. We could change this to instead show the union of all the keys, and in cases
// where there are multiple values, display something like "Values: 27" in the value field.
for (LWComponent c : groupContents) {
// getAll() will include merge-source data, tho it won't display in UI. But it WILL exclude
// OTHER meta-data if any is found, so we don't want that.
// final Collection<VueMetadataElement> data = c.getMetadataList().getAll();
final Collection<VueMetadataElement> data = c.getMetadataList().getCategoryList().getAsList();
if (data.isEmpty())
continue;
if (shared.isEmpty()) { // first time
shared.addAll(data);
} else {
shared.retainAll(data);
// If we're ever shrunk back to 0 size, we know we'll never find something in 100% of the
// items, and thus we're already done.
if (shared.isEmpty())
break;
}
}
if (DEBUG.Enabled) { Log.debug("shared: " + Util.tags(shared)); Util.dumpCollection(shared); }
if (shared.isEmpty()) {
model.ensureBulge("multi-is-empty");
} else {
// Normally, this should only go in the category list... we only need this for the
// data-model tho, so howbout we keep this as a separate list globally, and we never
// have to touch the sick meta-data API again after this, yes? The only real
// convenience we get from the LWGroup is being able to call getAllDescendents()...
final List<VueMetadataElement> shareTarget = group.getMetadataList().getAll();
for (VueMetadataElement vme : shared)
shareTarget.add(vme);
//if (DEBUG.Enabled) Log.debug("cats after update" + currentMultiples.getMetadataList().getMetadataAsHTML(VueMetadataElement.CATEGORY));
}
model.refresh();
}
public void activeChanged(ActiveEvent e) {
if (DEBUG.PAIN) Log.debug("activeChanged: " + e + " (now ignored)");
// if (e != null) ... if (active instanceof tufts.vue.LWSlide || active instanceof tufts.vue.LWMap) active = null;
}
private final tufts.vue.gui.VueButton headerAddButton = new tufts.vue.gui.VueButton("keywords.button.add");
private final JPanel headerAddButtonPanel = new JPanel();
private final JLabel LabelKeyword = new JLabel(VueResources.local("jlabel.keyword"));
private final JLabel LabelKeywordShared = new JLabel("Shared " + VueResources.local("jlabel.keyword")); // todo: localize
private final JLabel LabelCategory = new JLabel(VueResources.local("jlabel.category"));
private final JLabel LabelEmpty = new JLabel("");
private final Border HeaderCellBorder = GUI.makeSpace(0, CELL_HORZ_INSET+2, 0, 0);
private final TableCellRenderer HeaderRenderer = new MDTableHeaderRenderer();
private class MDTableHeaderRenderer extends DefaultTableCellRenderer {
// see below - getter could be supplied in stand alone class
//static tufts.vue.gui.VueButton button = new tufts.vue.gui.VueButton("keywords.button.add");
public MDTableHeaderRenderer() {
headerAddButton.setBorderPainted(false);
headerAddButton.setContentAreaFilled(false);
// headerAddButton.setBorder(javax.swing.BorderFactory.createEmptyBorder()); why?
headerAddButton.setSize(new java.awt.Dimension(5,5));
/*headerAddButton.addActionListener(new java.awt.event.ActionListener(){
public void actionPerformed(java.awt.event.ActionEvent e) {
if(DEBUG_LOCAL) System.out.println("MetadataEditor -- table header add button pressed " + e); } });*/
/*headerAddButton.addMouseListener(new java.awt.event.MouseAdapter(){ public void mousePressed(java.awt.event.MouseEvent e) {
if(DEBUG_LOCAL) System.out.println("MetadataEditor -- table header mouse pressed " + e); } } });*/
LabelKeyword.setFont(GUI.LabelFace);
LabelKeywordShared.setFont(GUI.LabelFace);
LabelCategory.setFont(GUI.LabelFace);
}
// can't do this statically in inner class but could be done from wholly separate class -
// for now move the button out into the metadata editor
/*static tufts.vue.gui.VueButton getButton() { return button; }*/
@Override public java.awt.Component getTableCellRendererComponent(JTable table, Object value,boolean isSelected,boolean hasFocus,int row,int col) {
JComponent comp = new JPanel();
if (col == TC_TEXT) {
// back to "Keywords:" -- VUE-953
comp = (group != null) ? LabelKeywordShared : LabelKeyword;
} else if (col == TC_COMBO) {
// back to "Categories" -- VUE-953
comp = LabelCategory;
} else if (col == TC_BUTTON) {
//((JLabel)comp).setIcon(tufts.vue.VueResources.getImageIcon("metadata.editor.add.up"));
comp = headerAddButtonPanel;
comp.add(headerAddButton);
} else
comp = LabelEmpty;
if (col == TC_BUTTON)
comp.setBorder(buttonBorder);
else
comp.setBorder(HeaderCellBorder);
comp.setOpaque(true);
comp.setBackground(MetadataEditor.this.getBackground());
return comp;
}
}
private static boolean hasKnownCategory(final String key) {
// A guess is good enough -- users can always add items manually via "Edit Categories"
return key != null && key.indexOf('#') > 0;
// if (keyName == null || keyName.indexOf('#') < 0)
// return false;
// // todo: crazy that CategoryModel doesn't have a hash lookup for this.
// for (edu.tufts.vue.ontology.Ontology ont : OntologiesList)
// for (OntType ontType : ont.getOntTypes())
// if (ontType.matchesKey(keyName))
// return true;
// // for (OntType ot : OntologiesList<>getOntTypes()) // any languages that do something like this?
// return false;
}
private class MDCellRenderer extends DefaultTableCellRenderer
{
private final JComponent deleteButton;
private final JPanel box = new JPanel();
private final JLabel addLabel = new JLabel("+");
private final JLabel faintKeyLabel = new JLabel();
private final JTextField renderEditable = new JTextField();
MDCellRenderer() {
setFont(GUI.LabelFace);
deleteButton = new edu.tufts.vue.metadata.gui.MetaButton(MetadataEditor.this, "renderDelete");
renderEditable.setFont(GUI.LabelFace);
renderEditable.setBorder(null);
box.setLayout(new BorderLayout());
box.setName("MD:cellRenderer");
if (DEBUG.BOXES) {
box.setOpaque(true);
box.setBackground(Color.yellow);
} else
box.setOpaque(false);
faintKeyLabel.setForeground(Color.lightGray);
faintKeyLabel.setFont(GUI.LabelFaceItalic);
addLabel.setForeground(Color.gray);
addLabel.setFont(tufts.vue.VueConstants.LargeFixedFontBold);
}
public java.awt.Component getTableCellRendererComponent(final JTable t, final Object _vme, boolean selected, boolean focus, final int row, final int col)
{
//final boolean isPlusOneEditRow = (PlusOneWhenAdding == 1 && row == model.getRowCount() - 1);
final boolean isPlusOneEditRow = false;
if (DEBUG.PAIN) Log.debug("gTCRC: bc=" + buttonColumn + (row<10?" ":" ") + row + "," + col + " " + _vme);
box.removeAll(); // this box is used as a safe generic objext we know we can stick a border on
if (TC_COMBO == col) {
final VueMetadataElement vme = (VueMetadataElement) _vme;
final String key = vme.getKey();
final boolean knownCategory = hasKnownCategory(key);
model.reportCategoryFound(row, knownCategory); // ick
setText(shortKey(key));
box.add(this);
if (DisplayChangesDuringEdit && row == activeEditRow) {
; // don't draw '+'
// if (mdTable.isEditing()) ; // turn them all off
} else if (!knownCategory)
box.add(addLabel, BorderLayout.EAST);
}
else if (TC_TEXT == col && (isPlusOneEditRow || _vme != null)) { // if value is null, we'll render empty box, even if PlusOneEditRow
final VueMetadataElement vme = (VueMetadataElement) _vme;
if (vme != null && vme.hasValue()) {
setText(vme.getValue());
box.add(this);
} else if (isPlusOneEditRow) {
// we're rendering the last row with an edit bulge: make it look inviting to activate an edit:
box.add(renderEditable);
} else {
if (!model.keyColumnVisible()) {
// the value is an empty string: display the key itself, faintly
if (vme.isKeyNone())
faintKeyLabel.setText("None");
else
faintKeyLabel.setText(shortKey(vme.getKey()));
box.add(faintKeyLabel);
} // else just render the empty box
}
}
else if (TC_BUTTON == col && (isPlusOneEditRow || _vme != null))
return deleteButton;
return installRenderBorder(box, row, col);
}
}
/**/ public final class ComboCE extends MDCellEditor { ComboCE() { super(0); } }
/**/ public final class TextCE extends MDCellEditor { TextCE() { super(1); } }
/**/ public final class ButtonCE extends MDCellEditor { ButtonCE() { super(2); } }
private static boolean ignoreCategorySelectEvents = false;
/** This was made as a global editor for all columns, which besides bad design, means that it makes it
impossiblefor one cell to stop editing in a single row, and another to begin. Would be nice to move
most functionality into the 3 different cell editor sub-classes */
private class MDCellEditor extends DefaultCellEditor
{
private final JTextField field;
private final JPanel box = new JPanel();
private final JComboBox catCombo = new JComboBox();
private final edu.tufts.vue.metadata.gui.MetaButton deleteButton;
private final int INSTANCE_COLUMN; // hard coded column we're meant to be rendering for
final String name;
private VueMetadataElement source_VME;
private MetadataList.SubsetList source_list;
public String toString() { return ""+INSTANCE_COLUMN; }
protected MDCellEditor(final int i) {
super(new JTextField());
INSTANCE_COLUMN = i;
name = getClass().getSimpleName() + "/" + i;
box.setName("MD:cellEditor:box-" + i);
box.setOpaque(false);
box.setLayout(new BorderLayout());
// box.addMouseListener(new tufts.vue.MouseAdapter("MDCE:JPanel-container-"+i) { public void mousePressed(MouseEvent e) {
// model.setSaved(activeEditRow, true);
// stopCellEditing();
// }});
if (INSTANCE_COLUMN == 0) {
initCatCombo();
field = null;
deleteButton = null;
} else if (INSTANCE_COLUMN == 1) {
field = (JTextField) getComponent();
field.setName("MD:cellEditor-" + name);
field.setFont(GUI.LabelFace);
field.addFocusListener(new FocusAdapter() {
public void focusLost(FocusEvent fe) {
if (DEBUG.Enabled) {
VUE.diagPush("FL" + i);
debug("focusLost (" + (editWasCanceled?"CANCELLED":"natural") + ") source_VME=" + source_VME);
if (DEBUG.PAIN && fe != null) debug("opposite: " + GUI.name(fe.getOppositeComponent()));
}
activeTextEdit = null;
if (editWasCanceled) {
model.clearBulge("canceled");
flush_UI_text();
} else {
saveChangesOnFocusLoss(fe);
}
activeEditRow = -1;
if (DisplayChangesDuringEdit && model.keyColumnVisible())
mdTable.repaintLater();
if (DEBUG.Enabled) VUE.diagPop();
}
public void focusGained(FocusEvent fe) {
if (DisplayChangesDuringEdit && model.keyColumnVisible())
mdTable.repaintLater();
}
});
field.addKeyListener(new KeyAdapter() { public void keyPressed(KeyEvent ke) {
// BTW, *sometimes* this works automatically in the JTextField, but for some reason, not always.
if (ke.getKeyCode() == KeyEvent.VK_ESCAPE)
mdTable.cancelAnyEditing();
}});
deleteButton = null;
} else { // if (INSTANCE_COLUMN == 2) {
field = null;
deleteButton = new edu.tufts.vue.metadata.gui.MetaButton(MetadataEditor.this, "realDelete");
}
}
private void debug(String s) { Log.debug(this.name + " " + s); }
private void initCatCombo() {
catCombo.putClientProperty("JComboBox.isTableCellEditor", Boolean.TRUE);
//catCombo.putClientProperty("JComboBox.isSquare", Boolean.TRUE); // won't be shown or no effect here
catCombo.setModel(CategoryMenuModel);
catCombo.setRenderer(new CategoryComboBoxRenderer());
catCombo.setFont(GUI.LabelFace);
//catCombo.setMaximumSize(new java.awt.Dimension(300, 20));
//if (INSTANCE_COLUMN == 0) box.add(catCombo);
// note: if this had been an action listener insted of an item listener, we may not have had to deal with the event disabling code
catCombo.addItemListener(new ItemListener() { public void itemStateChanged(final ItemEvent ie) {
// Note: this is being fired at from the INTERNAL select down in selectKnownCategory, and always has been!
// No wonder the combo-box has been so wiggy. We have to handle event ignoring for this to work.
if (ignoreCategorySelectEvents || ie.getStateChange() != ItemEvent.SELECTED)
return;
if (DEBUG.PAIN) debug("catCombo itemStateChanged " + Util.tags(ie.getItem()));
if (ie.getItem() instanceof edu.tufts.vue.metadata.gui.EditCategoryItem) {
if (mdTable.getCellEditor() == null) {
// We shouldn't see this now that events are disabled when editCategory refreshes the model.
if (DEBUG.Enabled) debug("ignoring itemStateChanged: no active editor");
} else {
popEditCategoryDialog(); // and block...
mdTable.stopAnyEditing();
refreshAll();
}
} else {
catComboHasMadeSelection(ie);
mdTable.stopAnyEditing();
}
}});
}
private void popEditCategoryDialog() {
// todo: re-recreating this each time is usually not a great idea -- we often end up with multiple objects
// hanging out there in the heap, still listening to events, sometimes doing nasty things when they get them
// that conflict with newer versions of the same components.
if (mdTable.getCellEditor() != MDCellEditor.this) throw new Error("what the hell in " + GUI.name(MDCellEditor.this));
final JDialog ecd = new JDialog(VUE.getApplicationFrame(), VueResources.local("dialog.editcat.title"));
ecd.setModal(true);
ecd.add(new CategoryEditor(ecd,catCombo,MetadataEditor.this,single,activeEditRow,activeEditCol));
ecd.setBounds(475,300,300,250);
ecd.setVisible(true); // We will now block until modal dialog returns...
Log.info("back from CategoryEditor dialog");
}
private void catComboHasMadeSelection(final ItemEvent ie)
{
final Object selected = catCombo.getSelectedItem();
if (selected instanceof OntType == false) {
// assume coming from selectKnownCategory, having picked a divider or something
return;
}
final OntType ontType = (OntType) catCombo.getSelectedItem();
final int row = activeEditRow;
final VueMetadataElement tableVME = model.getValueAt(row, -1); // column ignored
if (DEBUG.Enabled) Log.debug("OntType selected: " + ontType);
// if (DEBUG.PAIN || tableVME == null) debug("VME for last request row " + row + ": " + tableVME);
if (tableVME == null) {
if (DEBUG.Enabled) debug("aborting catCombo state change on null VME: probably model refresh");
return;
}
publishEdit(activeEditRow,
freshVME(ontType.getAsKey(), tableVME.getValue()),
"Metadata Category Key");
// final VueMetadataElement vme = freshVME(ontType.getAsKey(), tableVME.getValue());
// if (DEBUG.PAIN) debug("constructed new key in: " + vme);
// if (group != null) {
// publishGroupChange(row, vme);
// }
// else if (single != null) {
// if (single.getMetadataList().getCategoryListSize() > row) {
// single.getMetadataList().getCategoryList().set(row, vme);
// } else {
// Log.error("adding a new VME via category menu; should not happen: " + vme);
// single.getMetadataList().getMetadata().add(vme);
// }
// }
}
private boolean editWasCanceled = false;
@Override public boolean stopCellEditing() { return stopEditing(false); }
@Override public void cancelCellEditing() { stopEditing(true); }
private boolean stopEditing(boolean cancel) {
if (DEBUG.PAIN) { debug("stopCellEditing, " + (cancel?"CANCELLING":"saving")); if (DEBUG.META) Util.printClassTrace("!java"); }
editWasCanceled = cancel;
if (cancel) {
super.cancelCellEditing();
return true; // ignored
} else
return super.stopCellEditing();
}
private void load_UI_text(VueMetadataElement vme) {
source_list = null;
if (group != null)
source_list = group.getMetadataList().getCategoryList();
else if (single != null)
source_list = single.getMetadataList().getCategoryList();
else
throw new Error("can't load VME w/out source list: " + vme);
if (DEBUG.PAIN) debug("pushing to UI from " + vme);
if (vme == null) vme = InputVME;
//if (DEBUG.PAIN) Log.debug(name + " pushing to UI from " + vme + (vme.getValue() == EmptyValue ? " <TheEmptyValue>" : ""));
source_VME = vme;
if (vme != InputVME)
field.setText(vme.getValue());
else
if (DEBUG.PAIN) debug("re-using any old input text on InputVME: " + Util.tags(field.getText()));
// leave old text input as a handy copy: it will automatically all be selected for easy replacement
}
private void flush_UI_text() {
source_VME = null;
source_list = null;
field.setText("");
}
private void apply_UI_text(VueMetadataElement target) {
// source_VME = vme;
// field.setText(vme.getValue());
}
@Override public java.awt.Component getTableCellEditorComponent(final JTable table, final Object _vme, boolean isSelected, final int row, final int col)
{
if (DEBUG.PAIN) debug("getTableCellEditorComponent ic" + INSTANCE_COLUMN + ": " + row + "," + col + " " + _vme);
activeEditRow = row;
activeEditCol = col;
final VueMetadataElement vme = (VueMetadataElement) _vme;
if (col == TC_COMBO) {
//CategoryMenuModel.selectBestMatchQuietly(getKeyForRow(row));
CategoryMenuModel.selectBestMatchQuietly(vme.getKey());
return installRenderBorder(catCombo, row, col);
}
else if (col == TC_TEXT) {
//---------------------------------------------------------------------------------------------------------------
// THIS IS WHERE THE VueMetadataElement VALUE IS EXTRACTED FROM THE MODEL AND PUSHED TO THE UI
//---------------------------------------------------------------------------------------------------------------
load_UI_text( vme ); // copy into field
GUI.invokeAfterAWT(new Runnable() { public void run() {
// This object is not yet in the AWT hierarchy (we haven't even returned it yet). It
// normally will be alive be by the time this is run.
field.requestFocus();
if (vme == InputVME)
field.selectAll();
}});
MetadataEditor.this.activeTextEdit = this;
// field.setBorder(null); // this will remove the focus border and cause the field to fill the entire cell
return field;
}
else if (col == TC_BUTTON) {
// The way we currently detect a click on one of these buttons is to actually return it when an
// editor is requested, which is what the table will do if there's a click over the region.
// This is a pain in that we must return a button that looks exactly like the render button,
// including border, or the UI will refelect the irrelevant editor-active state for this cell.
// We also need, of course, to report to the button what row it's in when fetched so it knows
// what's being clicked on.
deleteButton.setRowForButtonClick(row);
if (getActiveTextEdit() != null)
deleteButton.setBackground(Color.white); // we'll cancel the edit instead of delete
else
deleteButton.setBackground(Color.yellow);
return deleteButton;
}
// if (LIMITED_FOCUS) field.addMouseListener(new MouseAdapter() { public void mouseExited(MouseEvent me) { stopCellEditing(); }});
// so this method had always returned a panel containing what was really needed -- that
// was causing the horrible two-double-clicks to focus problem for the value JTextField.
return box;
}
private void saveChangesOnFocusLoss(final FocusEvent fe)
{
//if (DEBUG.Enabled) debug("handleFieldFocusLost (" + (editWasCanceled?"CANCELLED":"natural") + ") source_VME=" + source_VME);
final int row = activeEditRow;
if (DEBUG.PAIN) debug("row of last editor requested: " + row);
final TableCellEditor editor = mdTable.getCellEditor();
if (DEBUG.PAIN) {
debug("getCellEd: " + Util.tags(editor) + (editor == null ? " (no longer editing)" : ""));
if (editor != null && editor != MDCellEditor.this) {
debug("is not us: " + Util.tags(MDCellEditor.this));
if (editor instanceof MDCellEditor)
debug("remote row: " + activeEditRow);
}
}
if (editor != null)
editor.stopCellEditing();
final String fieldText = field.getText();
final String trimText = fieldText.trim();
if (DEBUG.PAIN) {
debug("source_VME: " + source_VME);
debug("field-text: " + Util.tags(fieldText));
}
//----------------------------------------------------------------------------------------
// Check to see if a meaninful change is ready to be applied (if not, early return)
//----------------------------------------------------------------------------------------
if (source_VME == InputVME) {
if (trimText.length() == 0) {
// if (DEBUG.PAIN) Log.debug("do nothing, no new text");
model.clearBulge("nothing to do, no new text");
flush_UI_text(); // don't allow re-use
return;
}
// new to the model
if (DEBUG.PAIN) debug("new md item for model");
} else {
if (DEBUG.PAIN) debug("existing item being edited: " + source_VME);
// we have something to replace, tho it may be a fresh empty vme
//if (DEBUG.Enabled && isInputVME(real_VME)) Log.info("SEEING INPUT VME IN MODEL", new Throwable("HERE"));
//if (trimText.length() == 0 && isEmptyVME(real_VME)) {
if (trimText.equals(source_VME.getValue())) { // getValue can return null
// The trim() will allow us to eliminate whitespace on a field value by editing, but not add it.
model.clearBulge("nothing to do, no significant change");
flush_UI_text(); // don't allow re-use
return;
}
}
// Even if nothing is now selected, we still want to process up to this point to allow
// for the flush_UI_text() calls above.
if (single == null && group == null)
return;
//----------------------------------------------------------------------------------------
// Actually apply a change:
//----------------------------------------------------------------------------------------
final VueMetadataElement new_VME;
if (source_VME == InputVME) {
if (DEBUG.Enabled) debug("at bottom with InputVME -- creating fresh -- ignoring any category menu selection");
// Leaving out the category should be fine (we don't have one in this case), as any attempt to
// click on the category menu (displaying "none" at this point), will focus-loss the text
// input, aborting the new line entirely if empty, or creating a new VME to then category edit
// if text is present.
new_VME = freshVME(trimText);
} else {
new_VME = freshVME(source_VME.getKey(), trimText); // copy over the old key
if (DEBUG.PAIN) debug("replacement VME with copied key: " + new_VME);
}
if (source_VME == InputVME) {
if (DEBUG.PAIN) debug("appending");
source_list.add(new_VME);
if (!model.clearBulge("success: filled bulge with new data"))
model.refresh(); // should never happen, but just in case
} else {
if (DEBUG.PAIN) debug("inserting new_VME at row " + row + " " + new_VME + " -> " + source_list);
// We're using ROW, which is from the current model, which is technically bad, because
// it could have changed if this was a focus-loss on selection change, but in that
// case, editing should have been canceled.
source_list.set(row, new_VME); // WE ALWAYS WRITE OVER THE OLD VME
// BUG: DOES NOT WORK IN THE GROUP CASE
}
// todo: handle in publish
// If we're not handling multiples, we're already done. If we are, we've only just changed
// the temporary group data, and we now need to publish the change to the selection.
if (groupContents != null) {
// final VueMetadataElement old = source_list.get(row);
for (LWComponent c : groupContents) {
final MetadataList md = c.getMetadataList();
if (source_VME == InputVME)
md.addElement(new_VME);
else
md.replaceValueForKey(new_VME);
c.layout();
}
}
if (single != null)
single.layout();
VUE.getActiveMap().markAsModified();
// todo: would be better to issue a model repaint event
VUE.getActiveViewer().repaint();
mdTable.repaint();
}
/** @inteface CellEditor */ @Override public boolean shouldSelectCell(EventObject o) { return false; }
/** @inteface CellEditor: "If editing can be started this method returns true." */
@Override public boolean isCellEditable(java.util.EventObject object) {
if (DEBUG.PAIN) { VUE.diagPush("isCellEditable"); Log.debug(object /*,new Throwable("HERE")*/ ); }
//boolean canEdit = canEditWithSideEffects(object);
boolean canEdit = canEdit(object);
if (DEBUG.PAIN) { Log.debug("canEdit = " + canEdit); VUE.diagPop(); }
return canEdit;
}
private boolean canEdit(java.util.EventObject object)
{
if (object == EDIT_REQUEST)
return true;
else if (object instanceof MouseEvent == false) {
if (DEBUG.Enabled) Log.debug("unexpected event input for isCellEditable: " + Util.tags(object));
return false;
}
final MouseEvent me = (MouseEvent) object;
final Point point = me.getPoint();
final int row = mdTable.rowAtPoint(point);
final int col = mdTable.columnAtPoint(point);
if (col == TC_BUTTON) {
if (DEBUG.PAIN) Log.debug("button-detect in isCellEditable, row " + row + ", col " + col);
// CLICK ON THE MINUS BUTTON -- if we actually miss the button itself in the cell, this will ensure an edit cancel.
// if (mdTable.getCellEditor() != null)
// mdTable.getCellEditor().cancelCellEditing();
return true;
}
else if (GUI.isDoubleClick(me)) {
if (DEBUG.PAIN) Log.debug("double-click detect in isCellEditable, row " + row + ", col " + col);
return true;
}
return false;
}
}
private JComponent installRenderBorder(JComponent c, int row, int col) {
c.setBorder(col == TC_BUTTON ? buttonBorder : insetBorder);
// final Border b = getRenderBorder(row, col);
return c;
}
private void publishEdit(int row, VueMetadataElement vme, String description)
{
final VueMetadataElement oldVME = model.getValueAt(row, -1);
if (DEBUG.Enabled) {
Log.debug(Util.tags(description) + "; publishing change to row " + row + ": " + oldVME);
Log.debug(Util.tags(description) + "; replacement for row " + row + ": " + vme);
}
if (group != null) {
for (LWComponent c : groupContents) {
final MetadataList.SubsetList md = c.getMetadataList().getCategoryList();
md.set(md.indexOf(oldVME), vme); // note: all will have same instance
if (DEBUG.Enabled) Log.debug("published to: " + c);
}
group.getMetadataList().getCategoryList().set(row, vme);
VUE.getActiveMap().notify(MetadataEditor.this, tufts.vue.LWKey.MetaData); // todo: undoable event
}
else if (single != null) {
single.getMetadataList().getCategoryList().set(row, vme); // better: model.setVmeAt
// if (single.getMetadataList().getCategoryListSize() > row) {
// single.getMetadataList().getCategoryList().set(row, vme);
// } else {
// Log.error("adding a new VME via category menu; should not happen: " + vme);
// single.getMetadataList().getMetadata().add(vme);
// }
single.notify(MetadataEditor.this, tufts.vue.LWKey.MetaData); // todo: undoable event
}
VUE.getActiveMap().markAsModified();
VUE.getActiveMap().getUndoManager().mark(description);
VUE.getActiveMap().notify(MetadataEditor.this, tufts.vue.LWKey.Repaint);
}
/**
* [DAN] watch out for current == null
* [SMF] So what getRowCount returns is presumably important for what the UI decides to draw, if anything.
* This only models the category (type 1) items from MetadataList.
*
* This is public for MetaButtonPanel
**/
public class MDTableModel extends AbstractTableModel
{
private final java.util.List<Boolean> categoryFound = new java.util.ArrayList<Boolean>(32);
private int cols = initCols(2);
private LWComponent source;
private int initCols(int cols) {
TC_BUTTON = buttonColumn = (cols - 1); // 1 or 2
TC_TEXT = TC_BUTTON - 1; // 0 or 1
TC_COMBO = TC_BUTTON - 2; // -1 or 0 (-1 if not visible)
return cols;
}
@Override public Class getColumnClass(int col) {
if (col == TC_TEXT)
return JTextField.class;
else if (col == TC_COMBO)
return JComboBox.class;
else
return Object.class;
}
@Override public void setValueAt(Object value, int row, int col) {
if (DEBUG.PAIN) Log.debug(" setValueAt " + row + "," + col + " " + Util.tags(value) + " (always ignored)");
/*fireTableDataChanged();*/
}
private boolean wasCategoryFound(int row) { return (row >= 0 && row < categoryFound.size()) ? categoryFound.get(row) : false; }
// CALLED DURING RENDERING OF THE COMBO-BOX COLUMN. Apparently is only used in one place for knowing if to render a tooltip or not.
// Is overkill complexity: could be re-calling some version of find/selectCategory based on the key found at row.
private void reportCategoryFound(int row, boolean found) {
if (categoryFound.size() <= row) // expand valid porition of capacity
for (int i = categoryFound.size(); i < row + 1; i++)
categoryFound.add(Boolean.TRUE);
categoryFound.set(row, found ? Boolean.TRUE : Boolean.FALSE);
}
/** @interface javax.swing.table.TableModel */
public int getRowCount() {
final int displayRows = getDataRowCount() + PlusOneWhenAdding;
// even if we're not adding, always render at least one empty row:
return displayRows > 0 ? displayRows : 1;
}
private int getDataRowCount() {
return source == null ? 0 : source.getMetadataList().getCategoryListSize();
}
/** @interface javax.swing.table.TableModel */
public int getColumnCount() { return cols; }
boolean keyColumnVisible() { return cols == 3; }
private void toggleCategoryKeyColumn() {
if (DEBUG.PAIN) Log.debug("model: toggleCategoryKeyColumn: cols <- " + cols);
if (this.cols == 2)
this.cols = initCols(3);
else
this.cols = initCols(2);
fireTableStructureChanged();
if (DEBUG.PAIN) Log.debug("model: toggleCategoryKeyColumn: cols -> " + cols);
}
public void refresh() {
this.source = (single != null ? single : group);
if (DEBUG.Enabled) Log.debug(getClass().getName() + ": refresh; src==" + source);
fireTableDataChanged();
}
// public void fireTableStructureChanged() { super.fireTableStructureChanged(); }
/* note: CellEditor also has isCellEditable (main entry point for mouse actions on
* metadatatable) actually the table has this method through its cell editor */
@Override public boolean isCellEditable(int row,int column) {
// old -- for combined view with ont types non editable -- now in seperate panel
//if(getValueAt(row,column) instanceof OntType) return false;
// deletebutton is now editable...
//return ( (column == buttonColumn -1) || (column == buttonColumn - 2) )
return true;
}
/** @interface javax.swing.table.TableModel */
public VueMetadataElement getValueAt(int row, int _column_ignored_)
{
final int size = getDataRowCount();
if (DEBUG.PAIN) Log.debug(" getValueAt" + (row<10?" ":" ") + row + "," + _column_ignored_ + " size=" + size + (PlusOneWhenAdding != 0 ? "+1":""));
if (source != null) {
if (size == 0 || row == size)
return InputVME;
else if (row >= size)
return null;
return source.getMetadataList().getCategoryListElement(row);
}
else Log.error(" getValueAt " + row + "," + _column_ignored_ + ": no data in model");
return null;
}
/**
* Set the model into a state of reporting having exactly one more piece of meta-data than the actual
* component/selection has in it, so that we can render/activate a cell-editor at the bottom without
* having to modify the actual component meta-data. Calls are idempotent, so we don't have to
* worry about overlapping / too many calls. Todo: move methods to MDTableModel.ensureBulge/clearBulge
*/
private boolean ensureBulge(String reason) {
if (DEBUG.PAIN) {
final String noneed = (PlusOneWhenAdding == 1) ? " (unneeded)" : "";
Log.debug(Util.color("temporary model bulge to +1: " + reason + noneed, Util.TERM_CYAN));
}
if (PlusOneWhenAdding == 0) {
PlusOneWhenAdding = 1;
refresh();
return true;
}
return false;
}
private boolean clearBulge(String reason) {
if (DEBUG.PAIN) {
final String noneed = (PlusOneWhenAdding == 0) ? " (unneeded)" : "";
Log.debug(Util.color("clearing the +1 model bulge: " + reason + noneed, Util.TERM_CYAN));
}
if (PlusOneWhenAdding != 0) {
PlusOneWhenAdding = 0;
refresh();
return true;
} else
return false;
}
public void deleteAtRow(final int row)
{
final VueMetadataElement vme = getValueAt(row, -1);
if (DEBUG.Enabled) Log.debug("deleteAtRow " + row + " " + vme);
if (row < 0 || row >= getDataRowCount()) {
// Checking getDataRowCount() is crucial, or we could end up secretly deleting OTHER data-list
// items, that are further down the list than the CATEOGRY items, such as merge-source data.
Log.warn("invalid row for delete: " + row);
return;
}
if (vme == InputVME || vme == null) {
Log.warn("invalid for delete: " + vme);
return;
}
if (groupContents != null) {
for (LWComponent c : groupContents) {
final List<VueMetadataElement> md = c.getMetadataList().getAll(); // could use subesetlist?
if (md.remove(vme)) {
if (DEBUG.Enabled) Log.debug("multiple deleted " + vme + "; " + c.getDiagnosticLabel());
if (md.size() == 0)
c.layout(); // no more meta-data: icon display may change
} else {
Log.warn("multiple failed to remove " + vme + "; " + c.getDiagnosticLabel());
}
}
}
// We want the below for both source == single, where we'll delete data off the actual node, and
// source == group, where we'll just delete out of our table-model LWGroup holder.
if (DEBUG.Enabled) Log.debug("delete--row " + row + " " + vme);
// How did removing by row ever work? We're indexing into the full, unfiltered-by-type VME list.
// Well, if CATEGORY type really is maintained first in the list ("CategoryFirstList"), then this
// could work, but if it ISN'T, such as we suspect with RESOURCE type, then this will delete the
// wrong thing -- but the only thing with RESOURCE_CATEGORY VME types is currently the LWMap
// itself, so we're just getting lucky here.
// Note that we could just use the fetched getValueAt VME from the row as a delete-key source, as
// opposed to our row index, but there can be multiple VME's with the same key + value (which
// really isn't something we should allow, but we do). To make it feel right, if there are dupes,
// we need to delete the one at the actual row, instead of just the 1st one with that same
// key/value in the list.
final VueMetadataElement removed = source.getMetadataList().getAll().remove(row);
if (!vme.equals(removed))
Log.error("BAD DELETE: " + removed + " != " + vme);
refresh();
// todo: someday, this layout/repaint would be better triggered by some kind of model update
// event from MetadataLlist up through its LWComponent, which if we had could then
// even become undoable. Also, make these undoable events!
if (single != null) {
single.layout();
single.notify(MDTableModel.this, tufts.vue.LWKey.MetaData);
} else {
VUE.getActiveMap().notify(MDTableModel.this, tufts.vue.LWKey.MetaData);
}
VUE.getActiveMap().markAsModified();
VUE.getActiveMap().getUndoManager().mark("Remove Data");
}
}
public void listChanged() {
// This was implemented horribly, and we don't actually need it.
// THIS WAS FIRING, VERY FREQUENTLY, DURING DERSERIALIZATION AND SOMETIMES CAUSING
// AWT TO HANG (deep AWT cursor code hang?)
// So this was happening for EVERY SINGLE VueMetadataElement add/load ON EVERY SINGLE NODE.
// And since the impl has put an EMPTY VME on every node, that's at minimum once per
// LWComponent. And god forbid there are, say, 10 meta-data fields on a node from a
// data-set -- that means 10 times per node. So with a map that has, say 500 nodes, it
// means 5,000 (FIVE-THOUSAND) calls to the AWT to run freaking VALIDATE() deserializing
// such maps...
// Furthermore, after init, we don't actually edit VME meta-data at runtime outside of the UI,
// meaning we don't need this at all.
// tufts.Util.printStackTrace("listChanged");
// ((MetadataTableModel)mdTable.getModel()).refresh();
// validate();
}
// the following was for the MapInfoPanel usage of this component -- currently using a more direct approach
// with JTable filtering built into JDK 1.6 the following approach might make more sense (and if additional
// filtering is needed and/or additional flexibility/ modifiability of filters)
/*public class CreatorFilterModel extends MetadataTableModel {
private int firstCreatorRow = -1;
public int findFirstCreatorRow() {
if(current == null)
return -1;
// todo: put proper dublic core category here
//return current.getMetadataList().findCategory("");
return -1;
}
public int getRowCount() {
return super.getRowCount() - 1;
}
public Object getValueAt(int row,int column) {
findFirstCreatorRow();
if(row< firstCreatorRow)
return super.getValueAt(row,column);
else
return super.getValueAt(row-1,column);
}
}*/
}
// /** @return true if we can find a proper RDF style category for the given keyName in the JComboBox model -- will also select that item in the JComboBox */
// public static void selectKnownCategory(final String keyName, final JComboBox catCombo) {
// CategoryMenuModel.selectBestMatch(keyName);
// // ignoreCategorySelectEvents = true;
// // int index = -1;
// // try {
// // index = CategoryMenuModel.indexOfCategoryKey(keyName);
// // // If not found, default to the 1st separator:
// // catCombo.setSelectedIndex(index >= 0 ? index : 1);
// // } catch (Throwable t) {
// // Log.error("selectKnownCategory " + keyName + " " + catCombo, t);
// // return false;
// // } finally {
// // ignoreCategorySelectEvents = false;;
// // }
// // return index != -1;
// }
// /** holy christ -- this has major side effects -- very hairy to figure out -- apparenly being used to detect mouse click! */
// private boolean canEditWithSideEffects(java.util.EventObject object)
// {
// if (object == EDIT_REQUEST)
// return true;
// else if (object instanceof MouseEvent == false) {
// if (DEBUG.Enabled) Log.debug("unexpected event input for isCellEditable: " + Util.tags(object));
// return false;
// }
// final MouseEvent me = (MouseEvent) object;
// final Point point = me.getPoint();
// final int row = mdTable.rowAtPoint(point);
// final int col = mdTable.columnAtPoint(point);
// if (col == TC_BUTTON) {
// if (DEBUG.PAIN) Log.debug("button-detect in isCellEditable, row " + row + ", col " + col);
// // CLICK ON THE MINUS BUTTON -- if we actually miss the button itself in the cell, this will ensure an edit cancel.
// // if (mdTable.getCellEditor() != null)
// // mdTable.getCellEditor().cancelCellEditing();
// // returning true will allow us to "start editing". This way, the table will request edit "editor" for
// // this cell (the button), and once it has the editor, it will pass this mouse press on to the button.
// // If we return false, no button could get the event, and we'd have to catch this mouse event elsewhere.
// // Since the button is actually a single constant renderer style button, the design is to call
// // setRow on our meta button when it is requested, so it will know what row to fire for.
// return true;
// }
// else if (GUI.isDoubleClick(me)) { // || col == buttonColumn )
// if (DEBUG.PAIN) Log.debug("double-click detect in isCellEditable, row " + row + ", col " + col);
// // What's this do???
// model.setSaved(row, false);
// int lsr = model.lastSavedRow;
// if(lsr != row)
// model.setSaved(lsr,true);
// // model.refresh();
// // mdTable.repaint();
// return true;
// }
// else if (row != -1 && !model.getIsSaved(row)) {
// if (DEBUG.PAIN) Log.debug("any non-saved detect in isCellEditable, row " + row + ", col " + col);
// model.setSaved(row, false);
// return true;
// }
// return false;
// }
// /**
// * this was a hack to get borders without using a full JTable grid, but it's a giant messy pain to try and get
// * working just right, requires the further getIsSaved hack and all the places that state needs updating, and
// * there's no reason not to just use a the JTable grid. If we want the grid off the buttons at the right, we can
// * easily merge the buttons into the the text cell (dropping column three entirely) with a BorderLayout.EAST position
// * and a bit of extra mouse code to check for clicks in the right of that cell. */
// private Border getRenderBorder(int row, int col)
// {
// //if (DEBUG.BOXES) return DebugBorder;
// if (col == TC_BUTTON)
// return insetBorder;
// final boolean saved = model.getIsSaved(row);
// // The idea was, I think, if not saved, it's in an editor, and we don't want a border.
// // Smarter now: if we're fetching an editor, we'll ask for border or not expliticly
// // depending on editor impl, so always put the border on, which will probably
// // fix a bunch of border missing bugs.
// //if (saved) {
// if (true) {
// // if (true) return BorderFactory.createCompoundBorder(topLeftBox,insetBorder);
// // if (true) return BorderFactory.createCompoundBorder(fullBox,insetBorder);
// boolean aboveSaved = false;
// boolean belowSaved = false;
// if (row > 0)
// aboveSaved = model.getIsSaved(row - 1);
// else //if (row == -1)
// aboveSaved = false;
// final int rowCount = model.getRowCount();
// if (row < rowCount) {
// belowSaved = model.getIsSaved(row + 1);
// //if (DEBUG.PAIN) Log.debug("last row, belowSaved = " + belowSaved);
// } else if (row >= rowCount - 1) {
// belowSaved = false;
// //if (DEBUG.PAIN) Log.debug("last row, belowSaved = " + belowSaved);
// }
// //if (DEBUG.PAIN) Log.debug("belowSaved = " + belowSaved);
// if (col == TC_TEXT) {
// if(!aboveSaved && !belowSaved) { return BorderFactory.createCompoundBorder(fullBox,insetBorderNoBottom); } // only seen at top
// else if( aboveSaved && !belowSaved) { return BorderFactory.createCompoundBorder(fullBox,insetBorderNoBottom); }
// else if(!aboveSaved && belowSaved) { return BorderFactory.createCompoundBorder(noBottomBox,insetBorder); }
// else if( aboveSaved && belowSaved) { return BorderFactory.createCompoundBorder(noBottomBox,insetBorder); }
// }
// else {
// if(!aboveSaved && !belowSaved) { return BorderFactory.createCompoundBorder(topLeftBotBox,insetBorderNoBottom); } // only seen at top
// else if( aboveSaved && !belowSaved) { return BorderFactory.createCompoundBorder(topLeftBotBox,insetBorderNoBottom); }
// else if(!aboveSaved && belowSaved) { return BorderFactory.createCompoundBorder(topLeftBox,insetBorder); }
// else if( aboveSaved && belowSaved) { return BorderFactory.createCompoundBorder(topLeftBox,insetBorder); }
// }
// }
// return insetBorder;
// }