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();
|
||||
|
||||
Friend[] getFriends();
|
||||
|
||||
boolean isClanMember(String name);
|
||||
|
||||
Preferences getPreferences();
|
||||
|
||||
@@ -67,4 +67,15 @@ public interface HiscoreConfig extends Config
|
||||
{
|
||||
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.GridLayout;
|
||||
import java.awt.Insets;
|
||||
import java.awt.event.KeyListener;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
@@ -343,6 +344,16 @@ public class HiscorePanel extends PluginPanel
|
||||
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)
|
||||
{
|
||||
if (result == null || result.getPlayer() == null)
|
||||
|
||||
@@ -81,6 +81,9 @@ public class HiscorePlugin extends Plugin
|
||||
private NavigationButton navButton;
|
||||
private HiscorePanel hiscorePanel;
|
||||
|
||||
@Inject
|
||||
private NameAutocompleter autocompleter;
|
||||
|
||||
@Provides
|
||||
HiscoreConfig provideConfig(ConfigManager configManager)
|
||||
{
|
||||
@@ -110,11 +113,16 @@ public class HiscorePlugin extends Plugin
|
||||
{
|
||||
menuManager.addPlayerMenuItem(LOOKUP);
|
||||
}
|
||||
if (config.autocomplete())
|
||||
{
|
||||
hiscorePanel.addInputKeyListener(autocompleter);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void shutDown() throws Exception
|
||||
{
|
||||
hiscorePanel.removeInputKeyListener(autocompleter);
|
||||
pluginToolbar.removeNavigation(navButton);
|
||||
menuManager.removePlayerMenuItem(LOOKUP);
|
||||
}
|
||||
@@ -130,6 +138,18 @@ public class HiscorePlugin extends Plugin
|
||||
{
|
||||
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 net.runelite.api.ChatMessageType;
|
||||
import net.runelite.api.ClanMember;
|
||||
import net.runelite.api.Friend;
|
||||
import net.runelite.api.GameState;
|
||||
import net.runelite.api.GrandExchangeOffer;
|
||||
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.RSClient;
|
||||
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.RSIndexedSprite;
|
||||
import net.runelite.rs.api.RSItemContainer;
|
||||
import net.runelite.rs.api.RSNPC;
|
||||
import net.runelite.rs.api.RSName;
|
||||
import net.runelite.rs.api.RSNameable;
|
||||
import net.runelite.rs.api.RSPlayer;
|
||||
import net.runelite.rs.api.RSWidget;
|
||||
|
||||
@@ -578,6 +582,26 @@ public abstract class RSClientMixin implements RSClient
|
||||
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
|
||||
@Override
|
||||
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
|
||||
{
|
||||
@Import("friendContainer")
|
||||
RSFriendContainer getFriendContainer();
|
||||
|
||||
@Import("isFriended")
|
||||
boolean isFriended(RSName var1, boolean var2);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user