Merge pull request #2528 from pettenge/hiscore-autocomplete

Autocomplete name when looking up player on HiScores
This commit is contained in:
Adam
2018-05-11 14:08:55 -04:00
committed by GitHub
8 changed files with 368 additions and 1 deletions

View File

@@ -305,6 +305,8 @@ public interface Client extends GameEngine
ClanMember[] getClanMembers();
Friend[] getFriends();
boolean isClanMember(String name);
Preferences getPreferences();

View File

@@ -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;
}
}

View File

@@ -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)

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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)

View File

@@ -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
{
}

View File

@@ -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);
}