Merge pull request #2528 from pettenge/hiscore-autocomplete
Autocomplete name when looking up player on HiScores
This commit is contained in:
@@ -305,6 +305,8 @@ public interface Client extends GameEngine
|
|||||||
|
|
||||||
ClanMember[] getClanMembers();
|
ClanMember[] getClanMembers();
|
||||||
|
|
||||||
|
Friend[] getFriends();
|
||||||
|
|
||||||
boolean isClanMember(String name);
|
boolean isClanMember(String name);
|
||||||
|
|
||||||
Preferences getPreferences();
|
Preferences getPreferences();
|
||||||
|
|||||||
@@ -67,4 +67,15 @@ public interface HiscoreConfig extends Config
|
|||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@ConfigItem(
|
||||||
|
position = 4,
|
||||||
|
keyName = "autocomplete",
|
||||||
|
name = "Autocomplete",
|
||||||
|
description = "Predict names when typing a name to lookup"
|
||||||
|
)
|
||||||
|
default boolean autocomplete()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ import java.awt.GridBagConstraints;
|
|||||||
import java.awt.GridBagLayout;
|
import java.awt.GridBagLayout;
|
||||||
import java.awt.GridLayout;
|
import java.awt.GridLayout;
|
||||||
import java.awt.Insets;
|
import java.awt.Insets;
|
||||||
|
import java.awt.event.KeyListener;
|
||||||
import java.awt.event.MouseAdapter;
|
import java.awt.event.MouseAdapter;
|
||||||
import java.awt.event.MouseEvent;
|
import java.awt.event.MouseEvent;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
@@ -343,6 +344,16 @@ public class HiscorePanel extends PluginPanel
|
|||||||
add(endpointPanel);
|
add(endpointPanel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void addInputKeyListener(KeyListener l)
|
||||||
|
{
|
||||||
|
this.input.addKeyListener(l);
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeInputKeyListener(KeyListener l)
|
||||||
|
{
|
||||||
|
this.input.removeKeyListener(l);
|
||||||
|
}
|
||||||
|
|
||||||
private void changeDetail(String skillName, HiscoreSkill skill)
|
private void changeDetail(String skillName, HiscoreSkill skill)
|
||||||
{
|
{
|
||||||
if (result == null || result.getPlayer() == null)
|
if (result == null || result.getPlayer() == null)
|
||||||
|
|||||||
@@ -81,6 +81,9 @@ public class HiscorePlugin extends Plugin
|
|||||||
private NavigationButton navButton;
|
private NavigationButton navButton;
|
||||||
private HiscorePanel hiscorePanel;
|
private HiscorePanel hiscorePanel;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private NameAutocompleter autocompleter;
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
HiscoreConfig provideConfig(ConfigManager configManager)
|
HiscoreConfig provideConfig(ConfigManager configManager)
|
||||||
{
|
{
|
||||||
@@ -110,11 +113,16 @@ public class HiscorePlugin extends Plugin
|
|||||||
{
|
{
|
||||||
menuManager.addPlayerMenuItem(LOOKUP);
|
menuManager.addPlayerMenuItem(LOOKUP);
|
||||||
}
|
}
|
||||||
|
if (config.autocomplete())
|
||||||
|
{
|
||||||
|
hiscorePanel.addInputKeyListener(autocompleter);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void shutDown() throws Exception
|
protected void shutDown() throws Exception
|
||||||
{
|
{
|
||||||
|
hiscorePanel.removeInputKeyListener(autocompleter);
|
||||||
pluginToolbar.removeNavigation(navButton);
|
pluginToolbar.removeNavigation(navButton);
|
||||||
menuManager.removePlayerMenuItem(LOOKUP);
|
menuManager.removePlayerMenuItem(LOOKUP);
|
||||||
}
|
}
|
||||||
@@ -130,6 +138,18 @@ public class HiscorePlugin extends Plugin
|
|||||||
{
|
{
|
||||||
menuManager.addPlayerMenuItem(LOOKUP);
|
menuManager.addPlayerMenuItem(LOOKUP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.getKey().equals("autocomplete"))
|
||||||
|
{
|
||||||
|
if (config.autocomplete())
|
||||||
|
{
|
||||||
|
hiscorePanel.addInputKeyListener(autocompleter);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
hiscorePanel.removeInputKeyListener(autocompleter);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,267 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018, John Pettenger
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
* list of conditions and the following disclaimer.
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package net.runelite.client.plugins.hiscore;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import java.awt.event.KeyEvent;
|
||||||
|
import java.awt.event.KeyListener;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.swing.SwingUtilities;
|
||||||
|
import javax.swing.text.BadLocationException;
|
||||||
|
import javax.swing.text.Document;
|
||||||
|
import javax.swing.text.JTextComponent;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.runelite.api.ClanMember;
|
||||||
|
import net.runelite.api.Client;
|
||||||
|
import net.runelite.api.Friend;
|
||||||
|
import net.runelite.api.Player;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
class NameAutocompleter implements KeyListener
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Non-breaking space character.
|
||||||
|
*/
|
||||||
|
private static final String NBSP = Character.toString((char)160);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Character class for characters that cannot be in an RSN.
|
||||||
|
*/
|
||||||
|
private static final Pattern INVALID_CHARS = Pattern.compile("[^a-zA-Z0-9_ -]");
|
||||||
|
|
||||||
|
private final Client client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name currently being autocompleted.
|
||||||
|
*/
|
||||||
|
private String autocompleteName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pattern for the name currently being autocompleted.
|
||||||
|
*/
|
||||||
|
private Pattern autocompleteNamePattern;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private NameAutocompleter(@Nullable Client client)
|
||||||
|
{
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void keyPressed(KeyEvent e)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void keyReleased(KeyEvent e)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void keyTyped(KeyEvent e)
|
||||||
|
{
|
||||||
|
final JTextComponent input = (JTextComponent)e.getSource();
|
||||||
|
final String inputText = input.getText();
|
||||||
|
|
||||||
|
// Only autocomplete if the selection end is at the end of the text.
|
||||||
|
if (input.getSelectionEnd() != inputText.length())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Character to be inserted at the selection start.
|
||||||
|
final String charToInsert = Character.toString(e.getKeyChar());
|
||||||
|
|
||||||
|
// Don't attempt to autocomplete if the name is invalid.
|
||||||
|
// This condition is also true when the user presses a key like backspace.
|
||||||
|
if (INVALID_CHARS.matcher(charToInsert).find()
|
||||||
|
|| INVALID_CHARS.matcher(inputText).find())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we are already autocompleting.
|
||||||
|
if (autocompleteName != null && autocompleteNamePattern.matcher(inputText).matches())
|
||||||
|
{
|
||||||
|
if (isExpectedNext(input, charToInsert))
|
||||||
|
{
|
||||||
|
final int insertIndex = input.getSelectionStart();
|
||||||
|
SwingUtilities.invokeLater(() ->
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Insert the character and move the selection.
|
||||||
|
Document doc = input.getDocument();
|
||||||
|
doc.remove(insertIndex, 1);
|
||||||
|
doc.insertString(insertIndex, charToInsert, null);
|
||||||
|
input.select(insertIndex + 1, input.getSelectionEnd());
|
||||||
|
}
|
||||||
|
catch (BadLocationException ex)
|
||||||
|
{
|
||||||
|
log.warn("Could not insert character.", ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Prevent default behavior.
|
||||||
|
e.consume();
|
||||||
|
}
|
||||||
|
else // Character to insert does not match current autocompletion. Look for another name.
|
||||||
|
{
|
||||||
|
newAutocomplete(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else // Search for a name to autocomplete
|
||||||
|
{
|
||||||
|
newAutocomplete(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void newAutocomplete(KeyEvent e)
|
||||||
|
{
|
||||||
|
final JTextComponent input = (JTextComponent)e.getSource();
|
||||||
|
final String inputText = input.getText();
|
||||||
|
final String nameStart = inputText.substring(0, input.getSelectionStart()) + e.getKeyChar();
|
||||||
|
|
||||||
|
if (findAutocompleteName(nameStart))
|
||||||
|
{
|
||||||
|
// Assert this.autocompleteName != null
|
||||||
|
final String name = this.autocompleteName;
|
||||||
|
SwingUtilities.invokeLater(() ->
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
input.getDocument().insertString(
|
||||||
|
nameStart.length(),
|
||||||
|
name.substring(nameStart.length()),
|
||||||
|
null);
|
||||||
|
input.select(nameStart.length(), name.length());
|
||||||
|
}
|
||||||
|
catch (BadLocationException ex)
|
||||||
|
{
|
||||||
|
log.warn("Could not autocomplete name.", ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean findAutocompleteName(String nameStart)
|
||||||
|
{
|
||||||
|
final Pattern pattern;
|
||||||
|
Optional<String> autocompleteName;
|
||||||
|
|
||||||
|
// Pattern to match names that start with nameStart.
|
||||||
|
// Allows spaces to be represented as common whitespaces, underscores,
|
||||||
|
// hyphens, or non-breaking spaces.
|
||||||
|
// Matching non-breaking spaces is necessary because the API
|
||||||
|
// returns non-breaking spaces when a name has whitespace.
|
||||||
|
pattern = Pattern.compile(
|
||||||
|
"(?i)^" + nameStart.replaceAll("[ _-]", "[ _" + NBSP + "-]") + ".+?");
|
||||||
|
|
||||||
|
if (client == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
autocompleteName = Optional.empty();
|
||||||
|
|
||||||
|
// TODO: Search lookup history
|
||||||
|
|
||||||
|
Friend[] friends = client.getFriends();
|
||||||
|
if (friends != null)
|
||||||
|
{
|
||||||
|
autocompleteName = Arrays.stream(friends)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(Friend::getName)
|
||||||
|
.filter(n -> pattern.matcher(n).matches())
|
||||||
|
.findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search clan if a friend wasn't found
|
||||||
|
if (!autocompleteName.isPresent())
|
||||||
|
{
|
||||||
|
final ClanMember[] clannies = client.getClanMembers();
|
||||||
|
if (clannies != null)
|
||||||
|
{
|
||||||
|
autocompleteName = Arrays.stream(clannies)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(ClanMember::getUsername)
|
||||||
|
.filter(n -> pattern.matcher(n).matches())
|
||||||
|
.findFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search cached players if a clannie wasn't found.
|
||||||
|
if (!autocompleteName.isPresent())
|
||||||
|
{
|
||||||
|
final Player[] cachedPlayers = client.getCachedPlayers();
|
||||||
|
autocompleteName = Arrays.stream(cachedPlayers)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(Player::getName)
|
||||||
|
.filter(n -> pattern.matcher(n).matches())
|
||||||
|
.findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autocompleteName.isPresent())
|
||||||
|
{
|
||||||
|
this.autocompleteName = autocompleteName.get().replace(NBSP, " ");
|
||||||
|
this.autocompleteNamePattern = Pattern.compile(
|
||||||
|
"(?i)^" + this.autocompleteName.replaceAll("[ _-]", "[ _-]") + "$");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.autocompleteName = null;
|
||||||
|
this.autocompleteNamePattern = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return autocompleteName.isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isExpectedNext(JTextComponent input, String nextChar)
|
||||||
|
{
|
||||||
|
String expected;
|
||||||
|
if (input.getSelectionStart() < input.getSelectionEnd())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
expected = input.getText(input.getSelectionStart(), 1);
|
||||||
|
}
|
||||||
|
catch (BadLocationException ex)
|
||||||
|
{
|
||||||
|
log.warn("Could not get first character from input selection.", ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
expected = "";
|
||||||
|
}
|
||||||
|
return nextChar.equalsIgnoreCase(expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ import java.util.List;
|
|||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import net.runelite.api.ChatMessageType;
|
import net.runelite.api.ChatMessageType;
|
||||||
import net.runelite.api.ClanMember;
|
import net.runelite.api.ClanMember;
|
||||||
|
import net.runelite.api.Friend;
|
||||||
import net.runelite.api.GameState;
|
import net.runelite.api.GameState;
|
||||||
import net.runelite.api.GrandExchangeOffer;
|
import net.runelite.api.GrandExchangeOffer;
|
||||||
import net.runelite.api.GraphicsObject;
|
import net.runelite.api.GraphicsObject;
|
||||||
@@ -90,11 +91,14 @@ import static net.runelite.client.callback.Hooks.eventBus;
|
|||||||
import net.runelite.rs.api.RSClanMemberManager;
|
import net.runelite.rs.api.RSClanMemberManager;
|
||||||
import net.runelite.rs.api.RSClient;
|
import net.runelite.rs.api.RSClient;
|
||||||
import net.runelite.rs.api.RSDeque;
|
import net.runelite.rs.api.RSDeque;
|
||||||
|
import net.runelite.rs.api.RSFriendContainer;
|
||||||
|
import net.runelite.rs.api.RSFriendManager;
|
||||||
import net.runelite.rs.api.RSHashTable;
|
import net.runelite.rs.api.RSHashTable;
|
||||||
import net.runelite.rs.api.RSIndexedSprite;
|
import net.runelite.rs.api.RSIndexedSprite;
|
||||||
import net.runelite.rs.api.RSItemContainer;
|
import net.runelite.rs.api.RSItemContainer;
|
||||||
import net.runelite.rs.api.RSNPC;
|
import net.runelite.rs.api.RSNPC;
|
||||||
import net.runelite.rs.api.RSName;
|
import net.runelite.rs.api.RSName;
|
||||||
|
import net.runelite.rs.api.RSNameable;
|
||||||
import net.runelite.rs.api.RSPlayer;
|
import net.runelite.rs.api.RSPlayer;
|
||||||
import net.runelite.rs.api.RSWidget;
|
import net.runelite.rs.api.RSWidget;
|
||||||
|
|
||||||
@@ -578,6 +582,26 @@ public abstract class RSClientMixin implements RSClient
|
|||||||
return clanMemberManager != null ? (ClanMember[]) getClanMemberManager().getNameables() : null;
|
return clanMemberManager != null ? (ClanMember[]) getClanMemberManager().getNameables() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@Override
|
||||||
|
public Friend[] getFriends()
|
||||||
|
{
|
||||||
|
final RSFriendManager friendManager = getFriendManager();
|
||||||
|
if (friendManager == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final RSFriendContainer friendContainer = friendManager.getFriendContainer();
|
||||||
|
if (friendContainer == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
RSNameable[] nameables = friendContainer.getNameables();
|
||||||
|
return (Friend[]) nameables;
|
||||||
|
}
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@Override
|
@Override
|
||||||
public boolean isClanMember(String name)
|
public boolean isClanMember(String name)
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2018, Adam <Adam@sigterm.info>
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
* list of conditions and the following disclaimer.
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package net.runelite.rs.api;
|
||||||
|
|
||||||
|
public interface RSFriendContainer extends RSNameableContainer
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -29,6 +29,9 @@ import net.runelite.mapping.Import;
|
|||||||
|
|
||||||
public interface RSFriendManager extends FriendManager
|
public interface RSFriendManager extends FriendManager
|
||||||
{
|
{
|
||||||
|
@Import("friendContainer")
|
||||||
|
RSFriendContainer getFriendContainer();
|
||||||
|
|
||||||
@Import("isFriended")
|
@Import("isFriended")
|
||||||
boolean isFriended(RSName var1, boolean var2);
|
boolean isFriended(RSName var1, boolean var2);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user