blob: 0b0139060e95908838e1423596ce3f3b4ea63c90 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache 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.apache.org/licenses/LICENSE-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 org.netbeans.modules.properties;
import java.io.Serializable;
import javax.swing.event.TableModelEvent;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableColumn;
import javax.swing.JTable;
import org.openide.util.NbBundle;
import org.openide.util.WeakListeners;
/**
* Model for the properties edit table. First column represents keys containing all .properties
* files belonging to bundle represnted by this model. Each next columns represents values
* for on .properties file from that bundle.
*
* @author Petr Jiricka
* @see BundleEditPanel
* @see javax.swing.table.AbstractTableModel
*/
public class PropertiesTableModel extends AbstractTableModel {
/** Generated serialized version UID. */
static final long serialVersionUID = -7882925922830244768L;
/** <code>PropertiesDataObject</code> this table presents. */
private BundleStructure structure;
/** Listens to changes on the bundle structure. */
private PropertyBundleListener bundleListener;
/** Create a data node for a given data object.
* The provided children object will be used to hold all child nodes.
* @param structure model to work with
*/
public PropertiesTableModel(BundleStructure structure) {
super();
this.structure = structure;
// listener for the BundleStructure
bundleListener = new TablePropertyBundleListener();
structure.addPropertyBundleListener(
(PropertyBundleListener) WeakListeners.create(PropertyBundleListener.class, bundleListener, structure)
);
}
/** Gets the class for a model. Overrides column class. */
@Override
public Class getColumnClass(int columnIndex) {
return StringPair.class;
}
/** Gets the number of rows in the model. Implements superclass abstract method. */
public int getRowCount() {
return structure.getKeyCount();
}
/** Gets the number of columns in the model. Implements superclass abstract method. */
public int getColumnCount() {
return structure.getEntryCount() + 1;
}
/** Gets the value for the given row and column. Implements superclass abstract method. */
public Object getValueAt(int row, int column) {
BundleStructure bs = structure;
if(column == 0)
// Get StringPair for key.
return stringPairForKey(row);//bs.keyAt(row);
else {
// Get StringPair for value.
Element.ItemElem item;
try {
item = bs.getItem(column - 1, bs.keyAt(row));
} catch (ArrayIndexOutOfBoundsException aie) {
item = null;
}
return stringPairForValue(item);
}
}
/** Gets string pair for a key in an item (may be null). */
private StringPair stringPairForKey(int row) {
BundleStructure bs = structure;
Element.ItemElem item = bs.getItem(0, bs.keyAt(row));
StringPair sp;
if (item == null)
sp = new StringPair("", bs.keyAt(row), true); // NOI18N
else
sp = new StringPair(item.getComment(), bs.keyAt(row), true);
if (structure.getEntryCount() > 1)
sp.setCommentEditable(false);
return sp;
}
/** Gets string pair for a value in an item (may be null). */
private StringPair stringPairForValue(Element.ItemElem item) {
if (item == null)
// item doesnt't exist -> value is null
return new StringPair(null, null);
else
return new StringPair(item.getComment(), item.getValue());
}
/** Gets name for column. Overrides superclass method.
* @param column model index of column
* @return name for column */
@Override
public String getColumnName(int column) {
String leading;
// Construct label.
if(column == structure.getSortIndex())
// Place for drawing ascending/descending mark in renderer.
leading = " "; // NOI18N
else
leading = " "; // NOI18N
if(column == 0)
return leading+NbBundle.getBundle(PropertiesTableModel.class).getString("LAB_KeyColumnLabel");
else {
if(structure.getEntryCount() == 1)
return leading+NbBundle.getBundle(PropertiesTableModel.class).getString("LBL_ColumnValue");
else {
PropertiesFileEntry entry = structure.getNthEntry(column - 1);
return entry == null ? "" : leading+Util.getLocaleLabel(entry); // NOI18N
}
}
}
/** Sets the value at rowIndex and columnIndex. Overrides superclass method. */
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
// If values equals -> no change was made -> return immediatelly.
if (aValue.equals(getValueAt(rowIndex, columnIndex))) {
return;
}
// PENDING - set comment for all files
// Is key.
if (columnIndex == 0) {
BundleStructure bs = structure;
String oldValue = (String)bs.keyAt(rowIndex);
if (oldValue == null) {
return;
}
String newValue = ((StringPair)aValue).getValue();
if (newValue == null) { // Key can be an empty string
// Remove from all files.
return;
} else {
// Set in all files
for (int i=0; i < structure.getEntryCount(); i++) {
PropertiesFileEntry entry = structure.getNthEntry(i);
if (entry != null) {
PropertiesStructure ps = entry.getHandler().getStructure();
if (ps != null) {
// set the key
if (!oldValue.equals(newValue)) {
ps.renameItem(oldValue, newValue);
// this resorting is necessary only if this column index is same as
// column according the sort is performed, REFINE
structure.sort(-1);
}
// set the comment
if (i == 0) {
Element.ItemElem item = ps.getItem(newValue);
if (item != null && ((StringPair)aValue).isCommentEditable()) {
// only set if they differ
if (!item.getComment().equals(((StringPair)aValue).getComment()))
item.setComment(((StringPair)aValue).getComment());
}
}
}
}
}
}
} else {
// Property value.
PropertiesFileEntry entry = structure.getNthEntry(columnIndex - 1);
String key = structure.keyAt(rowIndex);
if (entry != null && key != null) {
PropertiesStructure ps = entry.getHandler().getStructure();
if (ps != null) {
Element.ItemElem item = ps.getItem(key);
if (item != null) {
item.setValue(((StringPair)aValue).getValue());
item.setComment(((StringPair)aValue).getComment());
// this resorting is necessary only if this column index is same as
// column according the sort is performed, REFINE
structure.sort(-1);
} else {
if ((((StringPair)aValue).getValue().length() > 0) || (((StringPair)aValue).getComment().length() > 0)) {
ps.addItem(key, ((StringPair)aValue).getValue(), ((StringPair)aValue).getComment());
// this resorting is necessary only if this column index is same as
// column according the sort is performed, REFINE
structure.sort(-1);
}
}
}
}
}
}
/** Overrides superclass method. Overrides superclass method.
* @return true for all cells */
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
if (columnIndex == 0) {
return !structure.isReadOnly();
} else {
PropertiesFileEntry entry = structure.getNthEntry(columnIndex-1);
if (entry != null)
return entry.getFile().canWrite();
return false;
}
}
/** Fires a TableModelEvent - change of one column */
public void fireTableColumnChanged(int index) {
int columnModelIndex = index;
// reset the header value as well
Object list[] = listenerList.getListenerList();
for (int i = 0; i < list.length; i++) {
if (list[i] instanceof JTable) {
JTable jt = (JTable)list[i];
try {
TableColumn column = jt.getColumnModel().getColumn(index);
columnModelIndex = column.getModelIndex();
column.setHeaderValue(jt.getModel().getColumnName(columnModelIndex));
} catch (ArrayIndexOutOfBoundsException abe) {
// only catch exception
}
jt.getTableHeader().repaint();
}
}
fireTableChanged(new TableModelEvent(this, 0, getRowCount() - 1, columnModelIndex));
}
/**
* Get FileEntry according to column in Table View
* @param column
* @return
*/
PropertiesFileEntry getFileEntry(int column) {
return structure.getNthEntry(column-1);
}
/** Overrides superclass method. */
@Override
public String toString() {
StringBuffer result = new StringBuffer();
result.append("------------------------------ TABLE MODEL DUMP -----------------------\n"); // NOI18N
for (int row = 0; row < getRowCount(); row ++) {
for (int column = 0; column < getColumnCount(); column ++) {
StringPair sp = (StringPair)getValueAt(row, column);
result.append("[" /*+ sp.getComment() + "," */+ sp.getValue() + "]"); // NOI18N
if (column == 0)
result.append(" : "); // NOI18N
else
if (column == getColumnCount() - 1)
result.append("\n"); // NOI18N
else
result.append(","); // NOI18N
}
}
result.append("---------------------------- END TABLE MODEL DUMP ---------------------\n"); // NOI18N
return result.toString();
}
/** Cancels editing in all listening JTables if appropriate. */
private void cancelEditingInTables(CancelSelector can) {
Object list[] = listenerList.getListenerList();
for(int i = 0; i < list.length; i++) {
if(list[i] instanceof BundleEditPanel.BundleTable) {
BundleEditPanel.BundleTable jt = (BundleEditPanel.BundleTable)list[i];
if (can.doCancelEditing(jt.getEditingRow(), jt.getEditingColumn())) {
jt.removeEditorSilent();
}
}
}
}
/** Gets <code>CancelSelector</code> for this table. */
private CancelSelector getDefaultCancelSelector() {
return new CancelSelector() {
/** Returns whether editing should be canceled for given row and column. */
public boolean doCancelEditing(int row, int column) {
return (row >= 0 && row < getRowCount() && column >= 0 && column < getColumnCount());
}
};
}
/** Interface which finds out whether editing should be canceled if given cell is edited. */
private static interface CancelSelector {
/** Returns whether editing should be canceled for given row and column. */
public boolean doCancelEditing(int row, int column);
} // End of interface CancelSelector.
/** Inner class. Listener for changes on bundle structure. */
private class TablePropertyBundleListener implements PropertyBundleListener {
public void bundleChanged(final PropertyBundleEvent evt) {
// quick patch for bug #13026
// (ensure visual updates are performed in AWT thread)
if (java.awt.EventQueue.isDispatchThread()) {
doBundleChanged(evt);
}
else {
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
doBundleChanged(evt);
}
});
}
}
private void doBundleChanged(PropertyBundleEvent evt) {
int changeType = evt.getChangeType();
if(changeType == PropertyBundleEvent.CHANGE_STRUCT) {
// Structure changed.
// Note: Normal way would be use the next commented out rows, which should do in effect
// the same thing like reseting the model, but it doesn't, therefore we reset the model directly.
cancelEditingInTables(getDefaultCancelSelector());
fireTableStructureChanged();
Object[] list = PropertiesTableModel.super.listenerList.getListenerList();
for(int i = 0; i < list.length; i++) {
if(list[i] instanceof JTable) {
//!!! strange comment this model should be only stateless proxy
// Its necessary to create new instance of model otherwise the 'old' model values would remain.
((JTable)list[i]).setModel(new PropertiesTableModel(PropertiesTableModel.this.structure));
}
}
} else if(changeType == PropertyBundleEvent.CHANGE_ALL) {
// All items changed (keyset).
cancelEditingInTables(getDefaultCancelSelector());
// reset all header values as well
Object[] list = PropertiesTableModel.super.listenerList.getListenerList();
for (int i = 0; i < list.length; i++) {
if (list[i] instanceof JTable) {
JTable jt = (JTable)list[i];
for (int j=0 ; j < jt.getColumnModel().getColumnCount(); j++) {
TableColumn column = jt.getColumnModel().getColumn(j);
// column.setHeaderValue(jt.getModel().getColumnName(column.getModelIndex()));
column.setHeaderValue(jt.getModel().getColumnName(j));
}
}
}
fireTableDataChanged();
} else if(changeType == PropertyBundleEvent.CHANGE_FILE) {
// File changed.
final int index = structure.getEntryIndexByFileName(evt.getEntryName());
//This mean file deleted
if (index == -1) {
fireTableStructureChanged();
// if (Boolean.getBoolean("netbeans.debug.exceptions")) // NOI18N
// (new Exception("Changed file not found")).printStackTrace(); // NOI18N
return;
}
cancelEditingInTables(new CancelSelector() {
public boolean doCancelEditing(int row, int column) {
if (!(row >= 0 && row < getRowCount() && column >= 0 && column < getColumnCount()))
return false;
return (column == index + 1);
}
});
// fireTableColumnChanged(index + 1);
fireTableStructureChanged();
} else if(changeType == PropertyBundleEvent.CHANGE_ITEM) {
// one item changed
final int index2 = structure.getEntryIndexByFileName(evt.getEntryName());
final int keyIndex = structure.getKeyIndexByName(evt.getItemName());
if(index2 == -1 || keyIndex == -1) {
if(Boolean.getBoolean("netbeans.debug.exceptions")) // NOI18N
(new Exception("Changed file not found")).printStackTrace(); // NOI18N
return;
}
cancelEditingInTables(new CancelSelector() {
public boolean doCancelEditing(int row, int column) {
if (!(row >= 0 && row < getRowCount() && column >= 0 && column < getColumnCount()))
return false;
return (column == index2 + 1 && row == keyIndex);
}
});
fireTableCellUpdated(keyIndex, index2 + 1);
}
}
} // End of inner class TablePropertyBundleListener.
/**
* Object for the value for one cell in a table view.
* It is used to represent either (comment, value) pair of an item, or a key for an item.
*/
static class StringPair implements Serializable {
/** Holds comment for this instance. */
private String comment;
/** Key or value string depending on the <code>keyType</code>. */
private String value;
/** Type of instance. */
private boolean keyType;
/** Flag if comment is editable for this instance. */
private boolean commentEditable;
/** Generated serial version UID. */
static final long serialVersionUID =-463968846283787181L;
/** Constructs with empty comment and value. */
public StringPair() {
this (null, "", false); // NOI18N
}
/** Constructs with the given value and no comment. */
public StringPair(String v) {
this (null, v, true);
}
/** Constructs with the given comment and value. */
public StringPair(String c, String v) {
this (c, v, false);
}
/** Constructs with the given comment and value. */
public StringPair(String c, String v, boolean kt) {
comment = c;
value = v;
keyType = kt;
commentEditable = true;
}
/** @return comment associated with this element. */
public String getComment() {
return comment;
}
/** @return the value associated with this element. */
public String getValue() {
return value;
}
/** Overrides superclass method. */
@Override
public boolean equals(Object obj) {
if(obj == null || !(obj instanceof StringPair))
return false;
StringPair compared = (StringPair)obj;
// PENDING compare keyTypes as well?
// Compare commnents first.
if(comment == null && compared.getComment() != null)
return false;
String str1 = comment;
String str2 = compared.getComment();
if(!str1.equals(str2))
return false;
// Compare values.
if(value == null && compared.getValue() != null)
return false;
str1 = value;
str2 = compared.getValue();
return str1.equals(str2);
}
@Override
public int hashCode() {
int hash = 3;
hash = 19 * hash + (comment != null ? comment.hashCode() : 0);
hash = 19 * hash + (value != null ? value.hashCode() : 0);
return hash;
}
/** Overrides superclass method. */
@Override
public String toString() {
return value;
}
/** Returns the type key/value of the pair. */
public boolean isKeyType () {
return keyType;
}
/** @return true if comment should be allowed for editing. */
public boolean isCommentEditable() {
return commentEditable;
}
/** Sets whether the comment should be allowed to be edited. */
public void setCommentEditable(boolean newEditable) {
commentEditable = newEditable;
}
} // End of nested class StringPair.
}