Refactor incremental search (FindAsYouType) into ItemView class

This commit is contained in:
drdev
2014-01-05 19:59:07 +00:00
parent 8b0e20aa9e
commit adeec5d6a9
4 changed files with 222 additions and 205 deletions

View File

@@ -17,14 +17,7 @@
*/
package forge.gui.deckeditor;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
@@ -33,17 +26,10 @@ import java.util.Map.Entry;
import javax.swing.JPopupMenu;
import javax.swing.KeyStroke;
import javax.swing.Popup;
import javax.swing.PopupFactory;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.StringUtils;
import forge.Command;
import forge.Singletons;
import forge.deck.DeckBase;
@@ -59,9 +45,7 @@ import forge.gui.framework.FScreen;
import forge.gui.framework.ICDoc;
import forge.gui.match.controllers.CDetail;
import forge.gui.match.controllers.CPicture;
import forge.gui.toolbox.FLabel;
import forge.gui.toolbox.FMouseAdapter;
import forge.gui.toolbox.FSkin;
import forge.gui.toolbox.itemmanager.ItemManager;
import forge.gui.toolbox.itemmanager.SItemManagerIO;
import forge.gui.toolbox.itemmanager.SItemManagerIO.EditorPreference;
@@ -83,7 +67,6 @@ public enum CDeckEditorUI implements ICDoc {
private final HashMap<FScreen, ACEditorBase<? extends InventoryItem, ? extends DeckBase>> screenChildControllers;
private ACEditorBase<? extends InventoryItem, ? extends DeckBase> childController;
private boolean isFindingAsYouType = false;
private CDeckEditorUI() {
screenChildControllers = new HashMap<FScreen, ACEditorBase<? extends InventoryItem, ? extends DeckBase>>();
@@ -344,7 +327,7 @@ public enum CDeckEditorUI implements ICDoc {
catTable.getComponent().addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (!isFindingAsYouType && KeyEvent.VK_SPACE == e.getKeyCode()) {
if (!catView.isIncrementalSearchActive() && KeyEvent.VK_SPACE == e.getKeyCode()) {
addSelectedCards(e.isControlDown() || e.isMetaDown(), e.isShiftDown() ? 4: 1);
}
else if (KeyEvent.VK_LEFT == e.getKeyCode() || KeyEvent.VK_RIGHT == e.getKeyCode()) {
@@ -363,7 +346,7 @@ public enum CDeckEditorUI implements ICDoc {
deckTable.getComponent().addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (!isFindingAsYouType && KeyEvent.VK_SPACE == e.getKeyCode()) {
if (!catView.isIncrementalSearchActive() && KeyEvent.VK_SPACE == e.getKeyCode()) {
removeSelectedCards(e.isControlDown() || e.isMetaDown(), e.isShiftDown() ? 4: 1);
}
else if (KeyEvent.VK_LEFT == e.getKeyCode() || KeyEvent.VK_RIGHT == e.getKeyCode()) {
@@ -422,26 +405,6 @@ public enum CDeckEditorUI implements ICDoc {
}
});
final _FindAsYouType catFind = new _FindAsYouType(catView);
final _FindAsYouType deckFind = new _FindAsYouType(deckView);
catTable.getComponent().addFocusListener(new FocusAdapter() {
@Override
public void focusLost(FocusEvent arg0) {
catFind.cancel();
}
});
deckTable.getComponent().addFocusListener(new FocusAdapter() {
@Override
public void focusLost(FocusEvent arg0) {
deckFind.cancel();
}
});
// highlight items as the user types a portion of their names
catTable.getComponent().addKeyListener(catFind);
deckTable.getComponent().addKeyListener(deckFind);
//set card when selection changes
catView.addSelectionListener(new ListSelectionListener() {
@Override
@@ -481,153 +444,6 @@ public enum CDeckEditorUI implements ICDoc {
});
}
private class _FindAsYouType extends KeyAdapter {
private StringBuilder str = new StringBuilder();
private final FLabel popupLabel = new FLabel.Builder().fontAlign(SwingConstants.LEFT).opaque().build();
private boolean popupShowing = false;
private Popup popup;
private Timer popupTimer;
private final ItemManager<? extends InventoryItem> tableView;
static final int okModifiers = KeyEvent.SHIFT_MASK | KeyEvent.ALT_GRAPH_MASK;
public _FindAsYouType(ItemManager<? extends InventoryItem> tableView) {
this.tableView = tableView;
}
private void _setPopupSize() {
// resize popup to size of label (ensure there's room for the next character so the label
// doesn't show '...' in the time between when we set the text and when we increase the size
Dimension labelDimension = popupLabel.getPreferredSize();
Dimension popupDimension = new Dimension(labelDimension.width + 12, labelDimension.height + 4);
SwingUtilities.getRoot(popupLabel).setSize(popupDimension);
}
private void _findNextMatch(int startIdx, boolean reverse) {
int numItems = tableView.getTable().getCount();
if (0 == numItems) {
cancel();
return;
}
// find the next item that matches the string
startIdx %= numItems;
final int increment = reverse ? numItems - 1 : 1;
int stopIdx = (startIdx + numItems - increment) % numItems;
String searchStr = str.toString();
boolean found = false;
for (int idx = startIdx;; idx = (idx + increment) % numItems) {
if (StringUtils.containsIgnoreCase(tableView.getTable().getItemAtIndex(idx).getName(), searchStr)) {
tableView.getTable().setSelectedIndex(idx);
found = true;
break;
}
if (idx == stopIdx) {
break;
}
}
if (searchStr.isEmpty()) {
cancel();
return;
}
// show a popup with the current search string, highlighted in red if not found
popupLabel.setText(searchStr + " (hit Enter for next match, Esc to cancel)");
if (found) {
FSkin.get(popupLabel).setForeground(FSkin.getColor(FSkin.Colors.CLR_TEXT));
}
else {
FSkin.get(popupLabel).setForeground(new Color(255, 0, 0));
}
if (popupShowing) {
_setPopupSize();
popupTimer.restart();
} else {
PopupFactory factory = PopupFactory.getSharedInstance();
Point tableLoc = tableView.getTable().getTable().getTableHeader().getLocationOnScreen();
popup = factory.getPopup(null, popupLabel, tableLoc.x + 10, tableLoc.y + 10);
FSkin.get(SwingUtilities.getRoot(popupLabel)).setBackground(FSkin.getColor(FSkin.Colors.CLR_INACTIVE));
popupTimer = new Timer(5000, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
cancel();
}
});
popupTimer.setRepeats(false);
popup.show();
_setPopupSize();
popupTimer.start();
isFindingAsYouType = true;
popupShowing = true;
}
}
public void cancel() {
str = new StringBuilder();
popupShowing = false;
if (null != popup) {
popup.hide();
popup = null;
}
if (null != popupTimer) {
popupTimer.stop();
popupTimer = null;
}
isFindingAsYouType = false;
}
@Override
public void keyPressed(KeyEvent e) {
if (KeyEvent.VK_ESCAPE == e.getKeyCode()) {
cancel();
}
}
@Override
public void keyTyped(KeyEvent e) {
switch (e.getKeyChar()) {
case KeyEvent.CHAR_UNDEFINED:
return;
case KeyEvent.VK_ENTER:
case 13: // no KeyEvent constant for this, but this comes up on OSX for shift-enter
if (!str.toString().isEmpty()) {
// no need to add (or subtract) 1 -- the table selection will already
// have been advanced by the (shift+) enter key
_findNextMatch(tableView.getTable().getSelectedIndex(), e.isShiftDown());
}
return;
case KeyEvent.VK_BACK_SPACE:
if (!str.toString().isEmpty()) {
str.deleteCharAt(str.toString().length() - 1);
}
break;
case KeyEvent.VK_SPACE:
// don't trigger if the first character is a space
if (str.toString().isEmpty()) {
return;
}
// fall through
default:
// shift and/or alt-graph down is ok. anything else is a hotkey (e.g. ctrl-f)
if (okModifiers != (e.getModifiers() | okModifiers)
|| !CharUtils.isAsciiPrintable(e.getKeyChar())) { // escape sneaks in here on Windows
return;
}
str.append(e.getKeyChar());
}
_findNextMatch(Math.max(0, tableView.getTable().getSelectedIndex()), false);
}
}
/* (non-Javadoc)
* @see forge.gui.framework.ICDoc#getCommandOnSelect()
*/

View File

@@ -141,6 +141,7 @@ public abstract class ItemManager<T extends InventoryItem> extends JPanel {
if (this.initialized) { return; } //avoid initializing more than once
//build table view
this.table.initialize();
this.viewScroller.setOpaque(false);
this.viewScroller.getViewport().setOpaque(false);
this.viewScroller.setBorder(null);
@@ -870,6 +871,16 @@ public abstract class ItemManager<T extends InventoryItem> extends JPanel {
return this.pnlButtons;
}
/**
*
* isIncrementalSearchActive.
*
* @return true if an incremental search is currently active
*/
public boolean isIncrementalSearchActive() {
return this.table.isIncrementalSearchActive();
}
/**
*
* getWantUnique.

View File

@@ -171,6 +171,11 @@ public final class ItemListView<T extends InventoryItem> extends ItemView<T> {
return this.table;
}
@Override
public Point getLocationOnScreen() {
return this.table.getTableHeader().getLocationOnScreen(); //use table header's location since that stays in place
}
@Override
protected String getCaption() {
return "List View";

View File

@@ -1,27 +1,62 @@
package forge.gui.toolbox.itemmanager.views;
import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JComponent;
import javax.swing.JViewport;
import javax.swing.Popup;
import javax.swing.PopupFactory;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.StringUtils;
import forge.gui.toolbox.FLabel;
import forge.gui.toolbox.FSkin;
import forge.gui.toolbox.itemmanager.ItemManager;
import forge.item.InventoryItem;
public abstract class ItemView<T extends InventoryItem> {
private final ItemManager<T> itemManager;
private boolean isIncrementalSearchActive = false;
protected ItemView(ItemManager<T> itemManager0) {
this.itemManager = itemManager0;
}
public void initialize() {
//hook incremental search functionality
final IncrementalSearch incrementalSearch = new IncrementalSearch();
this.getComponent().addFocusListener(new FocusAdapter() {
@Override
public void focusLost(FocusEvent arg0) {
incrementalSearch.cancel();
}
});
this.getComponent().addKeyListener(incrementalSearch);
}
public ItemManager<T> getItemManager() {
return this.itemManager;
}
public boolean isIncrementalSearchActive() {
return this.isIncrementalSearchActive;
}
public final T getSelectedItem() {
int index = getSelectedIndex();
return index >= 0 ? getItemAtIndex(index) : null;
@@ -102,6 +137,10 @@ public abstract class ItemView<T extends InventoryItem> {
return this.getComponent().hasFocus();
}
public Point getLocationOnScreen() {
return this.getComponent().getParent().getLocationOnScreen(); //use parent scroller's location by default
}
@Override
public String toString() {
return this.getCaption(); //return caption as string for display in combo box
@@ -121,4 +160,150 @@ public abstract class ItemView<T extends InventoryItem> {
protected abstract void onSetSelectedIndex(int index);
protected abstract void onSetSelectedIndices(Iterable<Integer> indices);
protected abstract void onScrollSelectionIntoView(JViewport viewport);
private class IncrementalSearch extends KeyAdapter {
private StringBuilder str = new StringBuilder();
private final FLabel popupLabel = new FLabel.Builder().fontAlign(SwingConstants.LEFT).opaque().build();
private boolean popupShowing = false;
private Popup popup;
private Timer popupTimer;
private static final int okModifiers = KeyEvent.SHIFT_MASK | KeyEvent.ALT_GRAPH_MASK;
public IncrementalSearch() {
}
private void setPopupSize() {
// resize popup to size of label (ensure there's room for the next character so the label
// doesn't show '...' in the time between when we set the text and when we increase the size
Dimension labelDimension = popupLabel.getPreferredSize();
Dimension popupDimension = new Dimension(labelDimension.width + 12, labelDimension.height + 4);
SwingUtilities.getRoot(popupLabel).setSize(popupDimension);
}
private void findNextMatch(int startIdx, boolean reverse) {
int numItems = itemManager.getItemCount();
if (0 == numItems) {
cancel();
return;
}
// find the next item that matches the string
startIdx %= numItems;
final int increment = reverse ? numItems - 1 : 1;
int stopIdx = (startIdx + numItems - increment) % numItems;
String searchStr = str.toString();
boolean found = false;
for (int idx = startIdx;; idx = (idx + increment) % numItems) {
if (StringUtils.containsIgnoreCase(ItemView.this.getItemAtIndex(idx).getName(), searchStr)) {
ItemView.this.setSelectedIndex(idx);
found = true;
break;
}
if (idx == stopIdx) {
break;
}
}
if (searchStr.isEmpty()) {
cancel();
return;
}
// show a popup with the current search string, highlighted in red if not found
popupLabel.setText(searchStr + " (hit Enter for next match, Esc to cancel)");
if (found) {
FSkin.get(popupLabel).setForeground(FSkin.getColor(FSkin.Colors.CLR_TEXT));
}
else {
FSkin.get(popupLabel).setForeground(new Color(255, 0, 0));
}
if (popupShowing) {
setPopupSize();
popupTimer.restart();
}
else {
PopupFactory factory = PopupFactory.getSharedInstance();
Point tableLoc = ItemView.this.getLocationOnScreen();
popup = factory.getPopup(null, popupLabel, tableLoc.x + 10, tableLoc.y + 10);
FSkin.get(SwingUtilities.getRoot(popupLabel)).setBackground(FSkin.getColor(FSkin.Colors.CLR_INACTIVE));
popupTimer = new Timer(5000, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
cancel();
}
});
popupTimer.setRepeats(false);
popup.show();
setPopupSize();
popupTimer.start();
isIncrementalSearchActive = true;
popupShowing = true;
}
}
public void cancel() {
str = new StringBuilder();
popupShowing = false;
if (null != popup) {
popup.hide();
popup = null;
}
if (null != popupTimer) {
popupTimer.stop();
popupTimer = null;
}
isIncrementalSearchActive = false;
}
@Override
public void keyPressed(KeyEvent e) {
if (KeyEvent.VK_ESCAPE == e.getKeyCode()) {
cancel();
}
}
@Override
public void keyTyped(KeyEvent e) {
switch (e.getKeyChar()) {
case KeyEvent.CHAR_UNDEFINED:
return;
case KeyEvent.VK_ENTER:
case 13: // no KeyEvent constant for this, but this comes up on OSX for shift-enter
if (!str.toString().isEmpty()) {
// no need to add (or subtract) 1 -- the table selection will already
// have been advanced by the (shift+) enter key
findNextMatch(ItemView.this.getSelectedIndex(), e.isShiftDown());
}
return;
case KeyEvent.VK_BACK_SPACE:
if (!str.toString().isEmpty()) {
str.deleteCharAt(str.toString().length() - 1);
}
break;
case KeyEvent.VK_SPACE:
// don't trigger if the first character is a space
if (str.toString().isEmpty()) {
return;
}
// fall through
default:
// shift and/or alt-graph down is ok. anything else is a hotkey (e.g. ctrl-f)
if (okModifiers != (e.getModifiers() | okModifiers)
|| !CharUtils.isAsciiPrintable(e.getKeyChar())) { // escape sneaks in here on Windows
return;
}
str.append(e.getKeyChar());
}
findNextMatch(Math.max(0, ItemView.this.getSelectedIndex()), false);
}
}
}