package com.revolsys.swing.field;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicInteger;
import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.JLabel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.ListSelectionModel;
import javax.swing.SwingConstants;
import javax.swing.border.Border;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.text.Document;
import org.jdesktop.swingx.JXBusyLabel;
import org.jdesktop.swingx.JXList;
import org.jdesktop.swingx.decorator.ColorHighlighter;
import org.jdesktop.swingx.decorator.ComponentAdapter;
import org.jdesktop.swingx.decorator.HighlightPredicate;
import org.jdesktop.swingx.decorator.HighlighterFactory;
import com.revolsys.awt.WebColors;
import com.revolsys.collection.map.LruMap;
import com.revolsys.datatype.DataType;
import com.revolsys.identifier.Identifier;
import com.revolsys.io.BaseCloseable;
import com.revolsys.io.PathName;
import com.revolsys.record.Record;
import com.revolsys.record.code.CodeTable;
import com.revolsys.record.query.BinaryCondition;
import com.revolsys.record.query.Condition;
import com.revolsys.record.query.Equal;
import com.revolsys.record.query.Q;
import com.revolsys.record.query.Query;
import com.revolsys.record.query.Value;
import com.revolsys.record.query.functions.F;
import com.revolsys.record.schema.RecordDefinition;
import com.revolsys.swing.Icons;
import com.revolsys.swing.SwingUtil;
import com.revolsys.swing.component.ValueField;
import com.revolsys.swing.layout.GroupLayouts;
import com.revolsys.swing.list.ArrayListModel;
import com.revolsys.swing.listener.ActionListenable;
import com.revolsys.swing.listener.WeakFocusListener;
import com.revolsys.swing.map.list.RecordListCellRenderer;
import com.revolsys.swing.menu.MenuFactory;
import com.revolsys.swing.parallel.Invoke;
import com.revolsys.util.Property;
import com.revolsys.value.ThreadBooleanValue;
public abstract class AbstractRecordQueryField extends ValueField
implements DocumentListener, KeyListener, MouseListener, FocusListener, ListDataListener,
ListSelectionListener, HighlightPredicate, ActionListenable {
private static final Icon ICON_DELETE = Icons.getIcon("delete");
private static final long serialVersionUID = 1L;
private final JXBusyLabel busyLabel = new JXBusyLabel(new Dimension(16, 16));
private final String displayFieldName;
private final ThreadBooleanValue eventsEnabled = new ThreadBooleanValue(true);
private final Map<Identifier, String> idToDisplayMap = new LruMap<>(100);
private final JXList list;
private final ArrayListModel<Record> listModel = new ArrayListModel<>();
private int maxResults = Integer.MAX_VALUE;
private final JPopupMenu menu = new JPopupMenu();
private int minSearchCharacters = 2;
private final JLabel oldValueItem;
private Identifier originalValue;
private final List<Query> queries;
private final TextField searchField = new TextField("search", 50);
private final AtomicInteger searchIndex = new AtomicInteger();
private Record selectedRecord;
private final PathName typePath;
public AbstractRecordQueryField(final String fieldName, final PathName typePath,
final String displayFieldName) {
super(fieldName, null);
this.typePath = typePath;
this.displayFieldName = displayFieldName;
this.queries = Arrays.asList(
new Query(typePath, new Equal(F.upper(displayFieldName), new Value(null))),
new Query(typePath, Q.iLike(displayFieldName, "")));
final Document document = this.searchField.getDocument();
document.addDocumentListener(this);
this.searchField.addFocusListener(new WeakFocusListener(this));
this.searchField.addKeyListener(this);
this.searchField.addMouseListener(this);
this.menu.setLayout(new BorderLayout(2, 2));
this.oldValueItem = new JLabel();
this.oldValueItem.addMouseListener(this);
this.oldValueItem.setForeground(new Color(128, 128, 128));
this.oldValueItem.setFont(SwingUtil.FONT);
this.oldValueItem.setHorizontalAlignment(SwingConstants.LEFT);
this.menu.add(this.oldValueItem, BorderLayout.NORTH);
this.list = new JXList(this.listModel);
this.list.setCellRenderer(new RecordListCellRenderer(displayFieldName));
this.list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
this.list.setHighlighters(HighlighterFactory.createSimpleStriping(Color.LIGHT_GRAY));
this.list.addMouseListener(this);
this.listModel.addListDataListener(this);
this.list.addListSelectionListener(this);
this.list.addHighlighter(new ColorHighlighter(this, WebColors.Blue, WebColors.White));
this.menu.add(new JScrollPane(this.list), BorderLayout.CENTER);
this.menu.setFocusable(false);
this.menu.setBorderPainted(true);
this.menu
.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(Color.DARK_GRAY),
BorderFactory.createEmptyBorder(1, 2, 1, 2)));
this.searchField.setEditable(true);
MenuFactory.getPopupMenuFactory(this.searchField);
this.searchField.setPreferredSize(new Dimension(100, 20));
add(this.searchField);
this.busyLabel.setVisible(false);
add(this.busyLabel);
GroupLayouts.makeColumns(this, false);
}
@Override
public void addActionListener(final ActionListener listener) {
this.searchField.addActionListener(listener);
}
@Override
public void changedUpdate(final DocumentEvent e) {
search();
}
@Override
public void contentsChanged(final ListDataEvent e) {
intervalAdded(e);
}
public BaseCloseable eventsDisabled() {
return this.eventsEnabled.closeable(false);
}
@Override
public void firePropertyChange(final String propertyName, final Object oldValue,
final Object newValue) {
super.firePropertyChange(propertyName, oldValue, newValue);
}
@Override
public void focusGained(final FocusEvent e) {
showMenu();
}
@Override
public void focusLost(final FocusEvent e) {
final Component oppositeComponent = e.getOppositeComponent();
if (oppositeComponent != this.list) {
this.menu.setVisible(false);
}
}
protected CodeTable getCodeTable() {
final RecordDefinition recordDefinition = getRecordDefinition();
final String fieldName = getFieldName();
if (recordDefinition == null) {
return null;
} else {
return recordDefinition.getCodeTableByFieldName(fieldName);
}
}
public String getDisplayFieldName() {
return this.displayFieldName;
}
protected String getDisplayText(final Identifier identifier) {
if (identifier == null || this.idToDisplayMap == null) {
return "";
} else {
final CodeTable codeTable = getCodeTable();
if (codeTable == null) {
String displayText = this.idToDisplayMap.get(identifier);
if (displayText == null) {
displayText = identifier.toString();
try {
final Record record = getRecord(identifier);
if (record != null) {
displayText = record.getString(this.displayFieldName);
}
} catch (final Exception e) {
}
}
return displayText;
} else {
return codeTable.getValue(identifier);
}
}
}
@SuppressWarnings("unchecked")
@Override
public <T> T getFieldValue() {
if (this.selectedRecord == null) {
return (T)super.getFieldValue();
} else {
return (T)this.selectedRecord.getIdentifier();
}
}
protected abstract Record getRecord(final Identifier identifier);
public abstract RecordDefinition getRecordDefinition();
protected abstract List<Record> getRecords(Query query);
public TextField getSearchField() {
return this.searchField;
}
public String getSearchText() {
return this.searchField.getText();
}
public Record getSelectedRecord() {
return this.selectedRecord;
}
public PathName getTypePath() {
return this.typePath;
}
@Override
public void insertUpdate(final DocumentEvent e) {
search();
}
@Override
public void intervalAdded(final ListDataEvent e) {
this.list.getSelectionModel().clearSelection();
}
@Override
public void intervalRemoved(final ListDataEvent e) {
intervalAdded(e);
}
@Override
public boolean isFieldValid() {
return true;
}
@Override
public boolean isHighlighted(final Component renderer, final ComponentAdapter adapter) {
final Record object = this.listModel.getElementAt(adapter.row);
final String text = this.searchField.getText();
final String value = object.getString(this.displayFieldName);
if (DataType.equal(text, value)) {
return true;
} else {
return false;
}
}
@Override
public void keyPressed(final KeyEvent e) {
final int keyCode = e.getKeyCode();
int increment = 1;
final int size = this.listModel.getSize();
int selectedIndex = this.list.getSelectedIndex();
switch (keyCode) {
case KeyEvent.VK_UP:
increment = -1;
case KeyEvent.VK_DOWN:
if (selectedIndex >= size) {
selectedIndex = -1;
}
selectedIndex += increment;
if (selectedIndex < 0) {
selectedIndex = 0;
} else if (selectedIndex >= size) {
selectedIndex = size - 1;
}
this.list.setSelectedIndex(selectedIndex);
setSelectedRecord(this.listModel.getElementAt(selectedIndex));
e.consume();
break;
case KeyEvent.VK_ENTER:
// if (size > 0) {
// if (selectedIndex >= 0 && selectedIndex < size) {
// final Record selectedRecord =
// this.listModel.getElementAt(selectedIndex);
// final String text = selectedRecord.getString(this.displayFieldName);
// if (!text.equals(this.getText())) {
// this.selectedItem = selectedRecord;
// setText(text);
// }
// } else {
// setText("");
// }
// }
return;
case KeyEvent.VK_TAB:
return;
default:
break;
}
showMenu();
}
@Override
public void keyReleased(final KeyEvent e) {
}
@Override
public void keyTyped(final KeyEvent e) {
}
@Override
public void mouseClicked(final MouseEvent e) {
if (e.getSource() == this.oldValueItem) {
if (e.getX() < 18) {
setFieldValue(null);
} else {
setFieldValue(this.originalValue);
}
this.menu.setVisible(false);
}
}
@Override
public void mouseEntered(final MouseEvent e) {
}
@Override
public void mouseExited(final MouseEvent e) {
}
@Override
public void mousePressed(final MouseEvent e) {
if (e.getSource() == this) {
showMenu();
}
}
@Override
public void mouseReleased(final MouseEvent e) {
}
protected void queryDo(final int searchIndex, final String queryText) {
if (searchIndex == this.searchIndex.get()) {
Record selectedRecord = null;
final Map<String, Record> allRecords = new TreeMap<>();
for (Query query : this.queries) {
if (allRecords.size() < this.maxResults) {
query = query.clone();
query.addOrderBy(this.displayFieldName, true);
final Condition whereCondition = query.getWhereCondition();
if (whereCondition instanceof BinaryCondition) {
final BinaryCondition binaryCondition = (BinaryCondition)whereCondition;
if (binaryCondition.getOperator().equalsIgnoreCase("like")) {
final String likeString = "%" + queryText.toUpperCase().replaceAll("[^A-Z0-9 ]", "%")
+ "%";
Q.setValue(0, binaryCondition, likeString);
} else {
Q.setValue(0, binaryCondition, queryText);
}
}
query.setLimit(this.maxResults);
final List<Record> records = getRecords(query);
for (final Record record : records) {
if (allRecords.size() < this.maxResults) {
final String key = record.getString(this.displayFieldName);
if (!allRecords.containsKey(key)) {
if (queryText.equals(key)) {
selectedRecord = record;
}
allRecords.put(key, record);
}
}
}
}
}
setSearchResults(searchIndex, allRecords.values(), selectedRecord);
}
}
@Override
public void removeActionListener(final ActionListener listener) {
this.searchField.removeActionListener(listener);
}
@Override
public void removeNotify() {
super.removeNotify();
this.menu.setVisible(false);
}
@Override
public void removeUpdate(final DocumentEvent e) {
search();
}
protected void search() {
if (this.eventsEnabled.isTrue()) {
final String queryText = this.searchField.getText();
if (Property.hasValue(queryText)) {
if (queryText.length() >= this.minSearchCharacters) {
this.searchField.setFieldValid();
this.busyLabel.setBusy(true);
this.busyLabel.setVisible(true);
final int searchIndex = this.searchIndex.incrementAndGet();
Invoke.background("search", () -> queryDo(searchIndex, queryText));
} else {
setSelectedRecord(null);
this.listModel.clear();
this.menu.setVisible(false);
this.searchField.setFieldInvalid(
"Minimum " + this.minSearchCharacters + " characters required for search",
WebColors.Red, WebColors.Pink);
}
} else {
this.listModel.clear();
this.menu.setVisible(false);
setSelectedRecord(null);
this.searchField.setFieldValid();
}
}
}
@Override
public void setFieldToolTip(final String toolTip) {
setToolTipText(toolTip);
}
@Override
public boolean setFieldValue(final Object value) {
final Identifier identifier = Identifier.newIdentifier(value);
super.setFieldValue(identifier);
final String displayText = getDisplayText(identifier);
if (this.searchField != null) {
this.searchField.setFieldValue(displayText);
}
this.originalValue = identifier;
Icon icon;
String originalText;
if (value == null) {
originalText = "-";
icon = null;
} else {
originalText = getDisplayText(this.originalValue);
icon = ICON_DELETE;
}
if (this.oldValueItem != null) {
this.oldValueItem.setIcon(icon);
this.oldValueItem.setText(originalText);
}
return true;
}
public void setMaxResults(final int maxResults) {
this.maxResults = maxResults;
}
public void setMinSearchCharacters(final int minSearchCharacters) {
this.minSearchCharacters = minSearchCharacters;
}
public void setSearchFieldBorder(final Border border) {
this.searchField.setBorder(border);
}
protected void setSearchResults(final int searchIndex, final Collection<Record> records,
final Record selectedRecord) {
Invoke.later(() -> {
if (searchIndex == this.searchIndex.get()) {
this.busyLabel.setBusy(false);
this.busyLabel.setVisible(false);
this.listModel.setAll(records);
if (isShowing()) {
showMenu();
setSelectedRecord(selectedRecord);
}
}
});
}
private void setSelectedRecord(final Record selectedRecord) {
final Record oldSelectedRecord = this.selectedRecord;
if (!DataType.equal(selectedRecord, oldSelectedRecord)) {
this.selectedRecord = selectedRecord;
firePropertyChange("selectedRecord", oldSelectedRecord, selectedRecord);
}
}
private void showMenu() {
final List<Record> records = this.listModel;
if (records.isEmpty()) {
this.menu.setVisible(false);
} else {
this.menu.setVisible(true);
int x;
int y;
x = 0;
final Insets screenInsets = Toolkit.getDefaultToolkit()
.getScreenInsets(getGraphicsConfiguration());
final Rectangle bounds = getGraphicsConfiguration().getBounds();
final int menuHeight = this.menu.getBounds().height;
final int screenY = getLocationOnScreen().y;
final int componentHeight = getHeight();
final int bottomOfMenu = screenY + menuHeight + componentHeight;
if (bottomOfMenu > bounds.height - screenInsets.bottom) {
y = -menuHeight;
} else {
y = componentHeight;
}
// } else {
// x = this.getWidth();
// y = 0;
// }
this.menu.show(this, x, y);
this.menu.pack();
}
}
@Override
public void updateFieldValue() {
}
@Override
public void valueChanged(final ListSelectionEvent e) {
if (!e.getValueIsAdjusting() && this.eventsEnabled.isTrue()) {
try (
final BaseCloseable eventsEnabled = eventsDisabled()) {
final Record record = (Record)this.list.getSelectedValue();
if (record != null) {
setSelectedRecord(record);
final Identifier identifier = record.getIdentifier();
final String label = record.getString(this.displayFieldName);
this.idToDisplayMap.put(identifier, label);
if (!DataType.equal(label, this.searchField.getText())) {
this.searchField.setFieldValue(label);
}
super.setFieldValue(identifier);
}
this.menu.setVisible(false);
this.searchField.requestFocus();
}
}
}
}