diff --git a/runelite-api/src/main/java/net/runelite/api/ScriptID.java b/runelite-api/src/main/java/net/runelite/api/ScriptID.java
index 849baf9139..0ca8d58b07 100644
--- a/runelite-api/src/main/java/net/runelite/api/ScriptID.java
+++ b/runelite-api/src/main/java/net/runelite/api/ScriptID.java
@@ -45,6 +45,13 @@ public final class ScriptID
*/
public static final int CHATBOX_INPUT = 96;
+ /**
+ * Layouts the bank widgets
+ *
+ * Takes 13 widget ids of various parts of the bank interface
+ */
+ public static final int BANK_LAYOUT = 277;
+
/**
* Closes the chatbox input
*
diff --git a/runelite-api/src/main/java/net/runelite/api/VarClientInt.java b/runelite-api/src/main/java/net/runelite/api/VarClientInt.java
index a599b1542e..1731b79046 100644
--- a/runelite-api/src/main/java/net/runelite/api/VarClientInt.java
+++ b/runelite-api/src/main/java/net/runelite/api/VarClientInt.java
@@ -42,6 +42,8 @@ public enum VarClientInt
*/
TOOLTIP_VISIBLE(2),
+ INPUT_TYPE(5),
+
MEMBERSHIP_STATUS(103),
WORLD_MAP_SEARCH_FOCUSED(190);
diff --git a/runelite-api/src/main/java/net/runelite/api/VarClientStr.java b/runelite-api/src/main/java/net/runelite/api/VarClientStr.java
index 4aa80b0c84..d872c55d64 100644
--- a/runelite-api/src/main/java/net/runelite/api/VarClientStr.java
+++ b/runelite-api/src/main/java/net/runelite/api/VarClientStr.java
@@ -34,7 +34,8 @@ import lombok.Getter;
@Getter
public enum VarClientStr
{
- CHATBOX_TYPED_TEXT(1);
+ CHATBOX_TYPED_TEXT(1),
+ INPUT_TEXT(22);
private final int index;
}
diff --git a/runelite-api/src/main/java/net/runelite/api/Varbits.java b/runelite-api/src/main/java/net/runelite/api/Varbits.java
index 87b2644ae2..c1b051040c 100644
--- a/runelite-api/src/main/java/net/runelite/api/Varbits.java
+++ b/runelite-api/src/main/java/net/runelite/api/Varbits.java
@@ -409,6 +409,8 @@ public enum Varbits
*/
CORP_DAMAGE(999),
+ CURRENT_BANK_TAB(4150),
+
WORLDHOPPER_FAVROITE_1(4597),
WORLDHOPPER_FAVROITE_2(4598);
diff --git a/runelite-api/src/main/java/net/runelite/api/vars/InputType.java b/runelite-api/src/main/java/net/runelite/api/vars/InputType.java
new file mode 100644
index 0000000000..08eb727c47
--- /dev/null
+++ b/runelite-api/src/main/java/net/runelite/api/vars/InputType.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2018, Ron Young
+ * 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.api.vars;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import net.runelite.api.VarClientInt;
+
+/**
+ * An enumeration of input types for {@link VarClientInt#INPUT_TYPE}.
+ */
+@Getter
+@RequiredArgsConstructor
+public enum InputType
+{
+ RUNELITE(-2),
+ NONE(0),
+ SEARCH(11);
+
+ private final int type;
+}
diff --git a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java
index 906d4b3563..fcad34babf 100644
--- a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java
+++ b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java
@@ -196,10 +196,14 @@ public class WidgetID
static class Bank
{
- static final int ITEM_CONTAINER = 12;
+ static final int BANK_CONTAINER = 1;
static final int INVENTORY_ITEM_CONTAINER = 3;
static final int BANK_TITLE_BAR = 4;
- static final int BANK_ITEM_COUNT = 5;
+ static final int CONTENT_CONTAINER = 9;
+ static final int ITEM_CONTAINER = 12;
+ static final int SEARCH_BUTTON_BACKGROUND = 39;
+ static final int INCINERATOR = 45;
+ static final int INCINERATOR_CONFIRM = 46;
}
static class GrandExchange
diff --git a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java
index 82d79b7bd9..8202c57dcc 100644
--- a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java
+++ b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java
@@ -119,10 +119,14 @@ public enum WidgetInfo
CLAN_CHAT_NAME(WidgetID.CLAN_CHAT_GROUP_ID, WidgetID.ClanChat.NAME),
CLAN_CHAT_OWNER(WidgetID.CLAN_CHAT_GROUP_ID, WidgetID.ClanChat.OWNER),
+ BANK_CONTAINER(WidgetID.BANK_GROUP_ID, WidgetID.Bank.BANK_CONTAINER),
+ BANK_SEARCH_BUTTON_BACKGROUND(WidgetID.BANK_GROUP_ID, WidgetID.Bank.SEARCH_BUTTON_BACKGROUND),
BANK_ITEM_CONTAINER(WidgetID.BANK_GROUP_ID, WidgetID.Bank.ITEM_CONTAINER),
BANK_INVENTORY_ITEMS_CONTAINER(WidgetID.BANK_INVENTORY_GROUP_ID, WidgetID.Bank.INVENTORY_ITEM_CONTAINER),
BANK_TITLE_BAR(WidgetID.BANK_GROUP_ID, WidgetID.Bank.BANK_TITLE_BAR),
- BANK_ITEM_COUNT(WidgetID.BANK_GROUP_ID, WidgetID.Bank.BANK_ITEM_COUNT),
+ BANK_INCINERATOR(WidgetID.BANK_GROUP_ID, WidgetID.Bank.INCINERATOR),
+ BANK_INCINERATOR_CONFIRM(WidgetID.BANK_GROUP_ID, WidgetID.Bank.INCINERATOR_CONFIRM),
+ BANK_CONTENT_CONTAINER(WidgetID.BANK_GROUP_ID, WidgetID.Bank.CONTENT_CONTAINER),
GRAND_EXCHANGE_WINDOW_CONTAINER(WidgetID.GRAND_EXCHANGE_GROUP_ID, WidgetID.GrandExchange.WINDOW_CONTAINER),
GRAND_EXCHANGE_OFFER_CONTAINER(WidgetID.GRAND_EXCHANGE_GROUP_ID, WidgetID.GrandExchange.OFFER_CONTAINER),
diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java
index 8affebbea6..17a9e1d971 100644
--- a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java
+++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java
@@ -244,6 +244,11 @@ public class ConfigManager
return t;
}
+ public List getConfigurationKeys(String prefix)
+ {
+ return properties.keySet().stream().filter(v -> ((String) v).startsWith(prefix)).map(String.class::cast).collect(Collectors.toList());
+ }
+
public String getConfiguration(String groupName, String key)
{
return properties.getProperty(groupName + "." + key);
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTagsConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTagsConfig.java
new file mode 100644
index 0000000000..07ed95dfdf
--- /dev/null
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTagsConfig.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2018, Ron Young
+ * 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.banktags;
+
+import net.runelite.client.config.Config;
+import net.runelite.client.config.ConfigGroup;
+import net.runelite.client.config.ConfigItem;
+
+@ConfigGroup("banktags")
+public interface BankTagsConfig extends Config
+{
+ @ConfigItem(
+ keyName = "useTabs",
+ name = "Use Tag Tabs",
+ description = "Enable the ability to add tabs to your bank which allow fast access to tags.",
+ position = 1
+ )
+ default boolean tabs()
+ {
+ return true;
+ }
+
+ @ConfigItem(
+ keyName = "position",
+ name = "",
+ description = "",
+ hidden = true
+ )
+ default int position()
+ {
+ return 0;
+ }
+
+ @ConfigItem(
+ keyName = "position",
+ name = "",
+ description = ""
+ )
+ void position(int idx);
+}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTagsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTagsPlugin.java
index 1abcd58f7c..3d46f59527 100644
--- a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTagsPlugin.java
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/BankTagsPlugin.java
@@ -1,5 +1,7 @@
/*
* Copyright (c) 2018, Adam
+ * Copyright (c) 2018, Ron Young
+ * Copyright (c) 2018, Tomas Slusny
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -24,9 +26,12 @@
*/
package net.runelite.client.plugins.banktags;
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
import com.google.common.eventbus.Subscribe;
+import com.google.inject.Provides;
+import java.awt.event.MouseWheelEvent;
import java.util.Arrays;
-import java.util.List;
import javax.inject.Inject;
import net.runelite.api.Client;
import net.runelite.api.InventoryID;
@@ -35,86 +40,100 @@ import net.runelite.api.ItemComposition;
import net.runelite.api.ItemContainer;
import net.runelite.api.MenuAction;
import net.runelite.api.MenuEntry;
+import net.runelite.api.events.ConfigChanged;
+import net.runelite.api.events.DraggingWidgetChanged;
+import net.runelite.api.events.GameTick;
import net.runelite.api.events.MenuEntryAdded;
import net.runelite.api.events.MenuOptionClicked;
import net.runelite.api.events.ScriptCallbackEvent;
+import net.runelite.api.events.WidgetLoaded;
import net.runelite.api.widgets.Widget;
+import net.runelite.api.widgets.WidgetID;
import net.runelite.api.widgets.WidgetInfo;
+import net.runelite.client.callback.ClientThread;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.game.ChatboxInputManager;
import net.runelite.client.game.ItemManager;
+import net.runelite.client.input.MouseManager;
+import net.runelite.client.input.MouseWheelListener;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
+import net.runelite.client.plugins.banktags.tabs.TabInterface;
+import net.runelite.client.plugins.banktags.tabs.TabSprites;
@PluginDescriptor(
name = "Bank Tags",
description = "Enable tagging of bank items and searching of bank tags",
tags = {"searching", "tagging"}
)
-public class BankTagsPlugin extends Plugin
+public class BankTagsPlugin extends Plugin implements MouseWheelListener
{
- private static final String CONFIG_GROUP = "banktags";
-
- private static final String ITEM_KEY_PREFIX = "item_";
+ public static final Splitter SPLITTER = Splitter.on(",").omitEmptyStrings().trimResults();
+ public static final Joiner JOINER = Joiner.on(",").skipNulls();
+ public static final String CONFIG_GROUP = "banktags";
+ public static final String TAG_SEARCH = "tag:";
+ public static final String EDIT_TAGS_MENU_OPTION = "Edit-tags";
+ public static final String ICON_SEARCH = "icon_";
private static final String SEARCH_BANK_INPUT_TEXT =
"Show items whose names or tags contain the following text:
" +
"(To show only tagged items, start your search with 'tag:')";
-
private static final String SEARCH_BANK_INPUT_TEXT_FOUND =
"Show items whose names or tags contain the following text: (%d found)
" +
"(To show only tagged items, start your search with 'tag:')";
- private static final String TAG_SEARCH = "tag:";
-
- private static final String EDIT_TAGS_MENU_OPTION = "Edit-tags";
-
- @Inject
- private Client client;
-
@Inject
private ItemManager itemManager;
@Inject
- private ConfigManager configManager;
+ private Client client;
+
+ @Inject
+ private ClientThread clientThread;
@Inject
private ChatboxInputManager chatboxInputManager;
- private String getTags(int itemId)
+ @Inject
+ private MouseManager mouseManager;
+
+ @Inject
+ private BankTagsConfig config;
+
+ @Inject
+ private TagManager tagManager;
+
+ @Inject
+ private TabInterface tabInterface;
+
+ @Provides
+ BankTagsConfig getConfig(ConfigManager configManager)
{
- String config = configManager.getConfiguration(CONFIG_GROUP, ITEM_KEY_PREFIX + itemId);
- if (config == null)
- {
- return "";
- }
- return config;
+ return configManager.getConfig(BankTagsConfig.class);
}
- private void setTags(int itemId, String tags)
+ @Override
+ public void startUp()
{
- if (tags == null || tags.isEmpty())
- {
- configManager.unsetConfiguration(CONFIG_GROUP, ITEM_KEY_PREFIX + itemId);
- }
- else
- {
- configManager.setConfiguration(CONFIG_GROUP, ITEM_KEY_PREFIX + itemId, tags);
- }
+ mouseManager.registerMouseWheelListener(this);
+ clientThread.invokeLater(tabInterface::init);
+ client.getSpriteOverrides().putAll(TabSprites.toMap(client));
}
- private int getTagCount(int itemId)
+ @Override
+ public void shutDown()
{
- String tags = getTags(itemId);
- if (tags.length() > 0)
+ mouseManager.unregisterMouseWheelListener(this);
+ clientThread.invokeLater(tabInterface::destroy);
+
+ for (TabSprites value : TabSprites.values())
{
- return tags.split(",").length;
+ client.getSpriteOverrides().remove(value.getSpriteId());
}
- return 0;
}
@Subscribe
- public void onScriptEvent(ScriptCallbackEvent event)
+ public void onScriptCallbackEvent(ScriptCallbackEvent event)
{
String eventName = event.getEventName();
@@ -141,29 +160,15 @@ public class BankTagsPlugin extends Plugin
case "bankSearchFilter":
int itemId = itemManager.canonicalize(intStack[intStackSize - 1]);
String itemName = stringStack[stringStackSize - 2];
- String searchInput = stringStack[stringStackSize - 1];
+ String search = stringStack[stringStackSize - 1];
- String tagsConfig = configManager.getConfiguration(CONFIG_GROUP, ITEM_KEY_PREFIX + itemId);
- if (tagsConfig == null || tagsConfig.length() == 0)
- {
- intStack[intStackSize - 2] = itemName.contains(searchInput) ? 1 : 0;
- return;
- }
-
- boolean tagSearch = searchInput.startsWith(TAG_SEARCH);
- String search;
+ boolean tagSearch = search.startsWith(TAG_SEARCH);
if (tagSearch)
{
- search = searchInput.substring(TAG_SEARCH.length()).trim();
- }
- else
- {
- search = searchInput;
+ search = search.substring(TAG_SEARCH.length()).trim();
}
- List tags = Arrays.asList(tagsConfig.toLowerCase().split(","));
-
- if (tags.stream().anyMatch(tag -> tag.contains(search.toLowerCase())))
+ if (tagManager.findTag(itemId, search))
{
// return true
intStack[intStackSize - 2] = 1;
@@ -173,54 +178,42 @@ public class BankTagsPlugin extends Plugin
intStack[intStackSize - 2] = itemName.contains(search) ? 1 : 0;
}
break;
+ case "getSearchingTagTab":
+ intStack[intStackSize - 1] = tabInterface.isActive() ? 1 : 0;
+ break;
}
}
@Subscribe
public void onMenuEntryAdded(MenuEntryAdded event)
{
- int widgetId = event.getActionParam1();
- if (widgetId != WidgetInfo.BANK_ITEM_CONTAINER.getId())
- {
- return;
- }
-
- int index = event.getActionParam0();
- if (index < 0)
- {
- return;
- }
-
- // Examine is the only guaranteed menuop to be added
- if (!"Examine".equals(event.getOption()))
- {
- return;
- }
-
- Widget container = client.getWidget(WidgetInfo.BANK_ITEM_CONTAINER);
- Widget item = container.getChild(index);
- int itemID = itemManager.canonicalize(item.getItemId());
-
- String text = EDIT_TAGS_MENU_OPTION;
-
- int tagCount = getTagCount(itemID);
- if (tagCount > 0)
- {
- text += " (" + tagCount + ")";
- }
-
- MenuEntry editTags = new MenuEntry();
- editTags.setParam0(event.getActionParam0());
- editTags.setParam1(event.getActionParam1());
- editTags.setTarget(event.getTarget());
- editTags.setOption(text);
- editTags.setType(MenuAction.RUNELITE.getId());
- editTags.setIdentifier(event.getIdentifier());
-
MenuEntry[] entries = client.getMenuEntries();
- entries = Arrays.copyOf(entries, entries.length + 1);
- entries[entries.length - 1] = editTags;
- client.setMenuEntries(entries);
+
+ if (event.getActionParam1() == WidgetInfo.BANK_ITEM_CONTAINER.getId()
+ && event.getOption().equals("Examine"))
+ {
+ Widget container = client.getWidget(WidgetInfo.BANK_ITEM_CONTAINER);
+ Widget item = container.getChild(event.getActionParam0());
+ int itemID = itemManager.canonicalize(item.getItemId());
+ String text = EDIT_TAGS_MENU_OPTION;
+ int tagCount = tagManager.getTags(itemID).size();
+ if (tagCount > 0)
+ {
+ text += " (" + tagCount + ")";
+ }
+ MenuEntry editTags = new MenuEntry();
+ editTags.setParam0(event.getActionParam0());
+ editTags.setParam1(event.getActionParam1());
+ editTags.setTarget(event.getTarget());
+ editTags.setOption(text);
+ editTags.setType(MenuAction.RUNELITE.getId());
+ editTags.setIdentifier(event.getIdentifier());
+ entries = Arrays.copyOf(entries, entries.length + 1);
+ entries[entries.length - 1] = editTags;
+ client.setMenuEntries(entries);
+ }
+
+ tabInterface.handleAdd(event);
}
@Subscribe
@@ -249,11 +242,9 @@ public class BankTagsPlugin extends Plugin
}
int itemId = itemManager.canonicalize(item.getId());
-
ItemComposition itemComposition = itemManager.getItemComposition(itemId);
String itemName = itemComposition.getName();
-
- String initialValue = getTags(itemId);
+ String initialValue = tagManager.getTagString(itemId);
chatboxInputManager.openInputWindow(itemName + " tags:", initialValue, (newTags) ->
{
@@ -261,9 +252,57 @@ public class BankTagsPlugin extends Plugin
{
return;
}
- setTags(itemId, newTags);
+
+ tagManager.setTagString(itemId, newTags);
});
}
+ else
+ {
+ tabInterface.handleClick(event);
+ }
}
+ @Subscribe
+ public void onConfigChanged(ConfigChanged configChanged)
+ {
+ if (configChanged.getGroup().equals("banktags") && configChanged.getKey().equals("useTabs"))
+ {
+ if (config.tabs())
+ {
+ clientThread.invokeLater(tabInterface::init);
+ }
+ else
+ {
+ clientThread.invokeLater(tabInterface::destroy);
+ }
+ }
+ }
+
+ @Subscribe
+ public void onGameTick(GameTick event)
+ {
+ tabInterface.update();
+ }
+
+ @Subscribe
+ public void onDraggingWidgetChanged(DraggingWidgetChanged event)
+ {
+ tabInterface.handleDrag(event.isDraggingWidget());
+ }
+
+ @Subscribe
+ public void onWidgetLoaded(WidgetLoaded event)
+ {
+ if (event.getGroupId() == WidgetID.BANK_GROUP_ID)
+ {
+ tabInterface.init();
+ }
+ }
+
+ @Override
+ public MouseWheelEvent mouseWheelMoved(MouseWheelEvent event)
+ {
+ clientThread.invokeLater(() -> tabInterface.handleWheel(event));
+ return event;
+ }
}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/TagManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/TagManager.java
new file mode 100644
index 0000000000..8aeeb38237
--- /dev/null
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/TagManager.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2018, Tomas Slusny
+ * Copyright (c) 2018, Ron Young
+ * 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.banktags;
+
+import com.google.common.base.Strings;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import net.runelite.client.config.ConfigManager;
+import static net.runelite.client.plugins.banktags.BankTagsPlugin.CONFIG_GROUP;
+import static net.runelite.client.plugins.banktags.BankTagsPlugin.JOINER;
+import static net.runelite.client.plugins.banktags.BankTagsPlugin.SPLITTER;
+import net.runelite.client.util.Text;
+
+@Singleton
+public class TagManager
+{
+ private static final String ITEM_KEY_PREFIX = "item_";
+ private final ConfigManager configManager;
+
+ @Inject
+ private TagManager(final ConfigManager configManager)
+ {
+ this.configManager = configManager;
+ }
+
+ public String getTagString(int itemId)
+ {
+ String config = configManager.getConfiguration(CONFIG_GROUP, ITEM_KEY_PREFIX + itemId);
+ if (config == null)
+ {
+ return "";
+ }
+
+ return config;
+ }
+
+ Collection getTags(int itemId)
+ {
+ return new LinkedHashSet<>(SPLITTER.splitToList(getTagString(itemId).toLowerCase()));
+ }
+
+ public void setTagString(int itemId, String tags)
+ {
+ if (Strings.isNullOrEmpty(tags))
+ {
+ configManager.unsetConfiguration(CONFIG_GROUP, ITEM_KEY_PREFIX + itemId);
+ }
+ else
+ {
+ configManager.setConfiguration(CONFIG_GROUP, ITEM_KEY_PREFIX + itemId, tags);
+ }
+ }
+
+ public void addTag(int itemId, String tag)
+ {
+ final Collection tags = getTags(itemId);
+ if (tags.add(Text.standardize(tag)))
+ {
+ setTags(itemId, tags);
+ }
+ }
+
+ private void setTags(int itemId, Collection tags)
+ {
+ setTagString(itemId, JOINER.join(tags));
+ }
+
+ boolean findTag(int itemId, String search)
+ {
+ return getTags(itemId).stream().anyMatch(tag -> tag.contains(Text.standardize(search)));
+ }
+
+ public void removeTag(String tag)
+ {
+ final String prefix = CONFIG_GROUP + "." + ITEM_KEY_PREFIX;
+ configManager.getConfigurationKeys(prefix).forEach(item -> removeTag(Integer.parseInt(item.replace(prefix, "")), tag));
+ }
+
+ public void removeTag(int itemId, String tag)
+ {
+ final Collection tags = getTags(itemId);
+ if (tags.remove(Text.standardize(tag)))
+ {
+ setTags(itemId, tags);
+ }
+ }
+}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TabInterface.java b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TabInterface.java
new file mode 100644
index 0000000000..7be53ded15
--- /dev/null
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TabInterface.java
@@ -0,0 +1,792 @@
+/*
+ * Copyright (c) 2018, Tomas Slusny
+ * Copyright (c) 2018, Ron Young
+ * 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.banktags.tabs;
+
+import com.google.common.base.Strings;
+import java.awt.Color;
+import java.awt.Rectangle;
+import java.awt.event.MouseWheelEvent;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import lombok.Getter;
+import net.runelite.api.Client;
+import net.runelite.api.InventoryID;
+import net.runelite.api.Item;
+import net.runelite.api.ItemComposition;
+import net.runelite.api.ItemContainer;
+import net.runelite.api.MenuAction;
+import net.runelite.api.MenuEntry;
+import net.runelite.api.Point;
+import net.runelite.api.ScriptID;
+import net.runelite.api.SoundEffectID;
+import net.runelite.api.SpriteID;
+import net.runelite.api.VarClientInt;
+import net.runelite.api.VarClientStr;
+import net.runelite.api.Varbits;
+import net.runelite.api.WidgetType;
+import net.runelite.api.events.MenuEntryAdded;
+import net.runelite.api.events.MenuOptionClicked;
+import net.runelite.api.vars.InputType;
+import net.runelite.api.widgets.Widget;
+import net.runelite.api.widgets.WidgetConfig;
+import net.runelite.api.widgets.WidgetInfo;
+import net.runelite.client.callback.ClientThread;
+import net.runelite.client.config.ConfigManager;
+import net.runelite.client.game.ChatboxInputManager;
+import net.runelite.client.game.ItemManager;
+import net.runelite.client.plugins.banktags.BankTagsConfig;
+import static net.runelite.client.plugins.banktags.BankTagsPlugin.CONFIG_GROUP;
+import static net.runelite.client.plugins.banktags.BankTagsPlugin.ICON_SEARCH;
+import static net.runelite.client.plugins.banktags.BankTagsPlugin.TAG_SEARCH;
+import net.runelite.client.plugins.banktags.TagManager;
+import net.runelite.client.util.ColorUtil;
+import net.runelite.client.util.Text;
+
+@Singleton
+public class TabInterface
+{
+ private static final Color HILIGHT_COLOR = Color.decode("#ff9040");
+ private static final String SCROLL_UP = "Scroll up";
+ private static final String SCROLL_DOWN = "Scroll down";
+ private static final String NEW_TAB = "New tag tab";
+ private static final String REMOVE_TAB = "Delete tag tab";
+ private static final String VIEW_TAB = "View tag tab";
+ private static final String CHANGE_ICON = "Change icon";
+ private static final String REMOVE_TAG = "Remove-tag";
+ private static final int TAB_HEIGHT = 40;
+ private static final int TAB_WIDTH = 39;
+ private static final int BUTTON_HEIGHT = 20;
+ private static final int MARGIN = 1;
+ private static final int SCROLL_TICK = 500;
+
+ // Widget indexes for searching
+ private static final int INNER_CONTAINER_IDX = 2;
+ private static final int SETTINGS_IDX = 4;
+ private static final int ITEM_CONTAINER_IDX = 7;
+ private static final int SCROLLBAR_IDX = 8;
+ private static final int BOTTOM_BAR_IDX = 9;
+ private static final int SEARCH_BUTTON_BACKGROUND_IDX = 15;
+ private static final int TITLE_BAR_IDX = 16;
+ private static final int ITEM_COUNT_IDX = 17;
+ private static final int TAB_BAR_IDX = 18;
+ private static final int INCINERATOR_IDX = 19;
+ private static final int INCINERATOR_CONFIRM_IDX = 20;
+ private static final int HIDDEN_WIDGET_IDX = 21;
+
+ private final Client client;
+ private final ClientThread clientThread;
+ private final ItemManager itemManager;
+ private final ConfigManager configManager;
+ private final TagManager tagManager;
+ private final TabManager tabManager;
+ private final ChatboxInputManager chatboxInputManager;
+ private final BankTagsConfig config;
+ private final Rectangle bounds = new Rectangle();
+ private final Rectangle canvasBounds = new Rectangle();
+
+ private TagTab activeTab;
+ private int maxTabs;
+ private int currentTabIndex;
+ private TagTab iconToSet = null;
+ private Instant startScroll = Instant.now();
+ private Object[] widgetIds;
+
+ @Getter
+ private Widget upButton;
+
+ @Getter
+ private Widget downButton;
+
+ @Getter
+ private Widget newTab;
+
+ @Getter
+ private Widget parent;
+
+ @Inject
+ private TabInterface(
+ final Client client,
+ final ClientThread clientThread,
+ final ItemManager itemManager,
+ final ConfigManager configManager,
+ final TagManager tagManager,
+ final TabManager tabManager,
+ final ChatboxInputManager chatboxInputManager,
+ final BankTagsConfig config)
+ {
+ this.client = client;
+ this.clientThread = clientThread;
+ this.itemManager = itemManager;
+ this.configManager = configManager;
+ this.tagManager = tagManager;
+ this.tabManager = tabManager;
+ this.chatboxInputManager = chatboxInputManager;
+ this.config = config;
+ }
+
+ public boolean isActive()
+ {
+ return activeTab != null;
+ }
+
+ public void init()
+ {
+ if (isHidden())
+ {
+ return;
+ }
+
+ Widget bankContainer = client.getWidget(WidgetInfo.BANK_CONTAINER);
+ widgetIds = bankContainer.getOnLoadListener();
+
+ currentTabIndex = config.position();
+ parent = client.getWidget(WidgetInfo.BANK_CONTENT_CONTAINER);
+
+ updateBounds();
+
+ upButton = createGraphic("", TabSprites.UP_ARROW.getSpriteId(), -1, TAB_WIDTH, BUTTON_HEIGHT, bounds.x, 0, true);
+ upButton.setAction(1, SCROLL_UP);
+ int clickmask = upButton.getClickMask();
+ clickmask |= WidgetConfig.DRAG;
+ upButton.setClickMask(clickmask);
+
+ downButton = createGraphic("", TabSprites.DOWN_ARROW.getSpriteId(), -1, TAB_WIDTH, BUTTON_HEIGHT, bounds.x, 0, true);
+ downButton.setAction(1, SCROLL_DOWN);
+ clickmask = downButton.getClickMask();
+ clickmask |= WidgetConfig.DRAG;
+ downButton.setClickMask(clickmask);
+
+ newTab = createGraphic("", TabSprites.NEW_TAB.getSpriteId(), -1, TAB_WIDTH, 39, bounds.x, 0, true);
+ newTab.setAction(1, NEW_TAB);
+
+ tabManager.clear();
+ tabManager.getAllTabs().forEach(this::loadTab);
+ activateTab(null);
+ scrollTab(0);
+ }
+
+ public void destroy()
+ {
+ activeTab = null;
+ currentTabIndex = 0;
+ maxTabs = 0;
+ parent = null;
+
+ if (upButton != null)
+ {
+ upButton.setHidden(true);
+ downButton.setHidden(true);
+ newTab.setHidden(true);
+ }
+
+ tabManager.clear();
+ }
+
+ public void update()
+ {
+ if (isHidden())
+ {
+ parent = null;
+
+ // If bank window was just hidden, update last active tab position
+ if (currentTabIndex != config.position())
+ {
+ config.position(currentTabIndex);
+ }
+
+ return;
+ }
+
+ String str = client.getVar(VarClientStr.INPUT_TEXT);
+
+ if (Strings.isNullOrEmpty(str))
+ {
+ str = "";
+ }
+
+ Widget bankTitle = client.getWidget(WidgetInfo.BANK_TITLE_BAR);
+ if (bankTitle != null && !bankTitle.isHidden())
+ {
+ str = bankTitle.getText().replaceFirst("Showing items: ", "");
+
+ if (str.startsWith("Tab "))
+ {
+ str = "";
+ }
+ }
+
+ str = Text.standardize(str);
+
+ if (str.startsWith("tag:"))
+ {
+ str = str.substring(4);
+ activateTab(tabManager.find(str));
+ }
+ else
+ {
+ activateTab(null);
+ }
+
+ updateBounds();
+ scrollTab(0);
+ }
+
+ public void handleWheel(final MouseWheelEvent event)
+ {
+ if (isHidden())
+ {
+ return;
+ }
+
+ if (canvasBounds.contains(event.getPoint()))
+ {
+ scrollTab(event.getWheelRotation());
+ }
+ }
+
+ public void handleAdd(MenuEntryAdded event)
+ {
+ if (isHidden())
+ {
+ return;
+ }
+
+ MenuEntry[] entries = client.getMenuEntries();
+ MenuEntry entry = entries[entries.length - 1];
+
+ if (activeTab != null
+ && event.getActionParam1() == WidgetInfo.BANK_ITEM_CONTAINER.getId()
+ && event.getOption().equals("Examine"))
+ {
+ MenuEntry removeTag = new MenuEntry();
+ removeTag.setParam0(event.getActionParam0());
+ removeTag.setParam1(event.getActionParam1());
+ removeTag.setTarget(event.getTarget());
+ removeTag.setOption(REMOVE_TAG + " (" + activeTab.getTag() + ")");
+ removeTag.setType(MenuAction.RUNELITE.getId());
+ removeTag.setIdentifier(event.getIdentifier());
+ entries = Arrays.copyOf(entries, entries.length + 1);
+ entries[entries.length - 1] = removeTag;
+ client.setMenuEntries(entries);
+ }
+ else if (iconToSet != null && (entry.getOption().startsWith("Withdraw-") || entry.getOption().equals("Release")))
+ {
+ // TODO: Do not replace every withdraw option with change icon option
+ entry.setOption(CHANGE_ICON + " (" + iconToSet.getTag() + ")");
+ client.setMenuEntries(entries);
+ }
+ }
+
+ public void handleClick(MenuOptionClicked event)
+ {
+ if (isHidden())
+ {
+ return;
+ }
+
+ if (iconToSet != null)
+ {
+ if (event.getMenuOption().startsWith(CHANGE_ICON))
+ {
+ ItemComposition item = getItem(event.getActionParam());
+ int itemId = itemManager.canonicalize(item.getId());
+ iconToSet.setIconItemId(itemId);
+ iconToSet.getIcon().setItemId(itemId);
+ configManager.setConfiguration(CONFIG_GROUP, ICON_SEARCH + iconToSet.getTag(), itemId + "");
+ event.consume();
+ }
+
+ // Reset icon selection even when we do not clicked item with icon
+ iconToSet = null;
+ }
+
+ if (activeTab != null
+ && event.getMenuOption().equals("Search")
+ && client.getWidget(WidgetInfo.BANK_SEARCH_BUTTON_BACKGROUND).getSpriteId() != SpriteID.EQUIPMENT_SLOT_SELECTED)
+ {
+ activateTab(null);
+ // This ensures that when clicking Search when tab is selected, the search input is opened rather
+ // than client trying to close it first
+ client.setVar(VarClientStr.INPUT_TEXT, "");
+ client.setVar(VarClientInt.INPUT_TYPE, 0);
+ }
+ else if (activeTab != null
+ && event.getMenuOption().startsWith("View tab"))
+ {
+ activateTab(null);
+ }
+ else if (activeTab != null
+ && event.getWidgetId() == WidgetInfo.BANK_ITEM_CONTAINER.getId()
+ && event.getMenuAction() == MenuAction.RUNELITE
+ && event.getMenuOption().startsWith(REMOVE_TAG))
+ {
+ // Add "remove" menu entry to all items in bank while tab is selected
+ event.consume();
+ final ItemComposition item = getItem(event.getActionParam());
+ final int itemId = itemManager.canonicalize(item.getId());
+ tagManager.removeTag(itemId, activeTab.getTag());
+ doSearch(InputType.SEARCH, TAG_SEARCH + activeTab.getTag());
+ }
+ else
+ {
+ switch (event.getMenuOption())
+ {
+ case SCROLL_UP:
+ event.consume();
+ scrollTab(-1);
+ break;
+ case SCROLL_DOWN:
+ event.consume();
+ scrollTab(1);
+ break;
+ case CHANGE_ICON:
+ event.consume();
+ iconToSet = tabManager.find(Text.removeTags(event.getMenuTarget()));
+ break;
+ case VIEW_TAB:
+ event.consume();
+ client.setVarbitValue(client.getVarps(), Varbits.CURRENT_BANK_TAB.getId(), 0);
+ Widget[] children = parent.getDynamicChildren();
+ Widget clicked = children[event.getActionParam()];
+
+ TagTab tab = tabManager.find(Text.removeTags(clicked.getName()));
+
+ if (tab.equals(activeTab))
+ {
+ resetSearch();
+
+ clientThread.invokeLater(() -> client.runScript(ScriptID.CLOSE_CHATBOX_INPUT));
+ }
+ else
+ {
+ openTag(TAG_SEARCH + Text.removeTags(clicked.getName()));
+ }
+
+ client.playSoundEffect(SoundEffectID.UI_BOOP);
+
+ break;
+ case NEW_TAB:
+ event.consume();
+ chatboxInputManager.openInputWindow("Tag Name", "", (tagName) ->
+ {
+ if (!Strings.isNullOrEmpty(tagName))
+ {
+ loadTab(tagName);
+ tabManager.save();
+ scrollTab(0);
+ }
+ });
+ break;
+ case REMOVE_TAB:
+ event.consume();
+ String target = Text.standardize(event.getMenuTarget());
+
+ // TODO: Replace this number input selection with actual in-game select input
+ chatboxInputManager.openInputWindow(
+ "1. Delete tab " + target + " and tag from all items
" +
+ "2. Delete tab " + target + "
" +
+ "3. Cancel", "", (response) ->
+ {
+ switch (response)
+ {
+ case "1":
+ tagManager.removeTag(target);
+ if (activeTab != null && activeTab.getTag().equals(target))
+ {
+ resetSearch();
+ }
+ case "2":
+ deleteTab(target);
+ break;
+ default:
+ break;
+ }
+ });
+ break;
+ }
+ }
+ }
+
+ public void handleDrag(boolean isDragging)
+ {
+ if (isHidden())
+ {
+ return;
+ }
+
+ Widget draggedOn = client.getDraggedOnWidget();
+ Widget draggedWidget = client.getDraggedWidget();
+
+ if (!isDragging || draggedOn == null)
+ {
+ return;
+ }
+
+ // is dragging widget and mouse button released
+ if (client.getMouseCurrentButton() == 0)
+ {
+ if (draggedWidget.getItemId() > 0 && draggedWidget.getId() != parent.getId())
+ {
+ // Tag an item dragged on a tag tab
+ if (draggedOn.getId() == parent.getId())
+ {
+ int itemId = itemManager.canonicalize(draggedWidget.getItemId());
+ tagManager.addTag(itemId, draggedOn.getName());
+ }
+ }
+ else if (parent.getId() == draggedOn.getId() && parent.getId() == draggedWidget.getId())
+ {
+ // Reorder tag tabs
+ if (!Strings.isNullOrEmpty(draggedOn.getName()))
+ {
+ tabManager.move(draggedWidget.getName(), draggedOn.getName());
+ tabManager.save();
+ updateTabs();
+ }
+ }
+ }
+ else if (draggedWidget.getItemId() > 0)
+ {
+ MenuEntry[] entries = client.getMenuEntries();
+
+ if (entries.length > 0)
+ {
+ MenuEntry entry = entries[entries.length - 1];
+
+ if (draggedWidget.getItemId() > 0 && entry.getOption().equals(VIEW_TAB) && draggedOn.getId() != draggedWidget.getId())
+ {
+ entry.setOption(TAG_SEARCH + Text.removeTags(entry.getTarget()));
+ entry.setTarget(draggedWidget.getName());
+ client.setMenuEntries(entries);
+ }
+
+ if (entry.getOption().equals(SCROLL_UP))
+ {
+ scrollTick(-1);
+ }
+ else if (entry.getOption().equals(SCROLL_DOWN))
+ {
+ scrollTick(1);
+ }
+ }
+ }
+ }
+
+ private void resetSearch()
+ {
+ doSearch(InputType.NONE, "");
+ }
+
+ private boolean isHidden()
+ {
+ Widget widget = client.getWidget(WidgetInfo.BANK_CONTAINER);
+ return !config.tabs() || widget == null || widget.isHidden();
+ }
+
+ private void loadTab(String tag)
+ {
+ TagTab tagTab = tabManager.load(tag);
+
+ if (tagTab.getBackground() == null)
+ {
+ Widget btn = createGraphic(ColorUtil.wrapWithColorTag(tagTab.getTag(), HILIGHT_COLOR), TabSprites.TAB_BACKGROUND.getSpriteId(), -1, TAB_WIDTH, TAB_HEIGHT, bounds.x, 1, true);
+ btn.setAction(1, VIEW_TAB);
+ btn.setAction(2, CHANGE_ICON);
+ btn.setAction(3, REMOVE_TAB);
+ tagTab.setBackground(btn);
+ }
+
+ if (tagTab.getIcon() == null)
+ {
+ Widget icon = createGraphic(ColorUtil.wrapWithColorTag(tagTab.getTag(), HILIGHT_COLOR), -1, tagTab.getIconItemId(), 36, 32, bounds.x + 3, 1, false);
+ int clickmask = icon.getClickMask();
+ clickmask |= WidgetConfig.DRAG;
+ clickmask |= WidgetConfig.DRAG_ON;
+ icon.setClickMask(clickmask);
+ icon.setDragDeadTime(5);
+ icon.setDragDeadZone(5);
+ tagTab.setIcon(icon);
+ }
+
+ tabManager.add(tagTab);
+ }
+
+ private void deleteTab(String tag)
+ {
+ if (activeTab != null && activeTab.getTag().equals(tag))
+ {
+ doSearch(InputType.SEARCH, "");
+ }
+
+ tabManager.remove(tag);
+ configManager.unsetConfiguration(CONFIG_GROUP, ICON_SEARCH + tag);
+ tabManager.save();
+
+ updateBounds();
+ scrollTab(0);
+ }
+
+ private void scrollTick(int direction)
+ {
+ // This ensures that dragging on scroll buttons do not scrolls too fast
+ if (startScroll.until(Instant.now(), ChronoUnit.MILLIS) >= SCROLL_TICK)
+ {
+ startScroll = Instant.now();
+ scrollTab(direction);
+ }
+ }
+
+ private void scrollTab(int direction)
+ {
+ maxTabs = (bounds.height - BUTTON_HEIGHT * 2 - MARGIN * 2) / TAB_HEIGHT;
+
+ // prevent running into the incinerator
+ while (bounds.y + maxTabs * TAB_HEIGHT + MARGIN * maxTabs + BUTTON_HEIGHT * 2 + MARGIN > bounds.y + bounds.height)
+ {
+ --maxTabs;
+ }
+
+ if (currentTabIndex + direction >= tabManager.size() || currentTabIndex + direction < 0)
+ {
+ currentTabIndex = 0;
+ }
+
+ if ((tabManager.size() - (currentTabIndex + direction) >= maxTabs) && (currentTabIndex + direction > -1))
+ {
+ currentTabIndex += direction;
+ }
+ else if (maxTabs < tabManager.size() && tabManager.size() - (currentTabIndex + direction) < maxTabs)
+ {
+ // Edge case when only 1 tab displays instead of up to maxTabs when one is deleted at the end of the list
+ currentTabIndex += direction;
+ scrollTab(-1);
+ }
+
+ updateTabs();
+ }
+
+ private void activateTab(TagTab tagTab)
+ {
+ if (activeTab != null && activeTab.equals(tagTab))
+ {
+ return;
+ }
+
+ if (activeTab != null)
+ {
+ Widget tab = activeTab.getBackground();
+ tab.setSpriteId(TabSprites.TAB_BACKGROUND.getSpriteId());
+ tab.revalidate();
+ activeTab = null;
+ }
+
+ if (tagTab != null)
+ {
+ Widget tab = tagTab.getBackground();
+ tab.setSpriteId(TabSprites.TAB_BACKGROUND_ACTIVE.getSpriteId());
+ tab.revalidate();
+ activeTab = tagTab;
+ }
+ }
+
+ private void updateBounds()
+ {
+ Widget itemContainer = client.getWidget(WidgetInfo.BANK_ITEM_CONTAINER);
+ if (itemContainer == null)
+ {
+ return;
+ }
+
+ int height = itemContainer.getHeight();
+
+ // If player isn't using normal bank tabs
+ if (itemContainer.getRelativeY() == 0)
+ {
+ height -= (TAB_HEIGHT + MARGIN);
+ }
+
+ bounds.setSize(TAB_WIDTH + MARGIN * 2, height);
+ bounds.setLocation(MARGIN, TAB_HEIGHT + MARGIN);
+
+ Widget incinerator = client.getWidget(WidgetInfo.BANK_INCINERATOR);
+
+ if (incinerator != null && !incinerator.isHidden())
+ {
+ // This is the required way to move incinerator, don't change it!
+ incinerator.setOriginalHeight(39);
+ incinerator.setOriginalWidth(48);
+ incinerator.setRelativeY(itemContainer.getHeight());
+ incinerator.revalidate();
+
+ Widget child = incinerator.getDynamicChildren()[0];
+ child.setHeight(39);
+ child.setWidth(48);
+ child.setType(WidgetType.GRAPHIC);
+ child.setSpriteId(TabSprites.INCINERATOR.getSpriteId());
+
+ bounds.setSize(TAB_WIDTH + MARGIN * 2, height - incinerator.getHeight());
+ }
+
+ if (upButton != null)
+ {
+ Point p = upButton.getCanvasLocation();
+ canvasBounds.setBounds(p.getX(), p.getY() + BUTTON_HEIGHT, bounds.width, maxTabs * TAB_HEIGHT + maxTabs * MARGIN);
+ }
+ }
+
+ private void updateTabs()
+ {
+ int y = bounds.y + MARGIN + BUTTON_HEIGHT;
+
+ if (maxTabs >= tabManager.size())
+ {
+ currentTabIndex = 0;
+ }
+ else
+ {
+ y -= (currentTabIndex * TAB_HEIGHT + currentTabIndex * MARGIN);
+ }
+
+ for (TagTab tab : tabManager.getTabs())
+ {
+ updateWidget(tab.getBackground(), y);
+ updateWidget(tab.getIcon(), y + 4);
+
+ // Edge case where item icon is 1 pixel out of bounds
+ tab.getIcon().setHidden(tab.getBackground().isHidden());
+
+ // Keep item widget shown while drag scrolling
+ if (client.getDraggedWidget() == tab.getIcon())
+ {
+ tab.getIcon().setHidden(false);
+ }
+
+ y += TAB_HEIGHT + MARGIN;
+ }
+
+ boolean hidden = !(tabManager.size() > 0);
+
+ upButton.setHidden(hidden);
+ upButton.setOriginalY(bounds.y);
+ upButton.revalidate();
+
+ downButton.setHidden(hidden);
+ downButton.setOriginalY(bounds.y + maxTabs * TAB_HEIGHT + MARGIN * maxTabs + BUTTON_HEIGHT + MARGIN);
+ downButton.revalidate();
+ }
+
+ private Widget createGraphic(String name, int spriteId, int itemId, int width, int height, int x, int y, boolean hasListener)
+ {
+ Widget widget = parent.createChild(-1, WidgetType.GRAPHIC);
+ widget.setOriginalWidth(width);
+ widget.setOriginalHeight(height);
+ widget.setOriginalX(x);
+ widget.setOriginalY(y);
+
+ widget.setSpriteId(spriteId);
+
+ if (itemId > -1)
+ {
+ widget.setItemId(itemId);
+ widget.setItemQuantity(-1);
+ widget.setBorderType(1);
+ }
+
+ if (hasListener)
+ {
+ widget.setOnOpListener(ScriptID.NULL);
+ widget.setHasListener(true);
+ }
+
+ widget.setName(name);
+ widget.revalidate();
+
+ return widget;
+ }
+
+ private void updateWidget(Widget t, int y)
+ {
+ t.setOriginalY(y);
+ t.setRelativeY(y);
+ t.setHidden(y < (bounds.y + BUTTON_HEIGHT + MARGIN) || y > (bounds.y + bounds.height - TAB_HEIGHT - MARGIN - BUTTON_HEIGHT));
+ t.revalidate();
+ }
+
+ private void doSearch(InputType inputType, String search)
+ {
+ // In case the widget ids array is incorrect, do not proceed
+ if (widgetIds == null || widgetIds.length < 21)
+ {
+ return;
+ }
+
+ clientThread.invokeLater(() ->
+ {
+ // This ensures that any chatbox input (e.g from search) will not remain visible when
+ // selecting/changing tab
+ client.runScript(ScriptID.CLOSE_CHATBOX_INPUT);
+
+ client.setVar(VarClientInt.INPUT_TYPE, inputType.getType());
+ client.setVar(VarClientStr.INPUT_TEXT, search);
+
+ client.runScript(ScriptID.BANK_LAYOUT,
+ WidgetInfo.BANK_CONTAINER.getId(),
+ widgetIds[INNER_CONTAINER_IDX],
+ widgetIds[SETTINGS_IDX],
+ widgetIds[ITEM_CONTAINER_IDX],
+ widgetIds[SCROLLBAR_IDX],
+ widgetIds[BOTTOM_BAR_IDX],
+ widgetIds[TITLE_BAR_IDX],
+ widgetIds[ITEM_COUNT_IDX],
+ widgetIds[SEARCH_BUTTON_BACKGROUND_IDX],
+ widgetIds[TAB_BAR_IDX],
+ widgetIds[INCINERATOR_IDX],
+ widgetIds[INCINERATOR_CONFIRM_IDX],
+ widgetIds[HIDDEN_WIDGET_IDX]);
+ });
+ }
+
+ private ItemComposition getItem(int idx)
+ {
+ ItemContainer bankContainer = client.getItemContainer(InventoryID.BANK);
+ Item item = bankContainer.getItems()[idx];
+ return itemManager.getItemComposition(item.getId());
+ }
+
+ private void openTag(String tag)
+ {
+ doSearch(InputType.SEARCH, tag);
+ activateTab(tabManager.find(tag.substring(4)));
+
+ // When tab is selected with search window open, the search window closes but the search button
+ // stays highlighted, this solves that issue
+ Widget searchBackground = client.getWidget(WidgetInfo.BANK_SEARCH_BUTTON_BACKGROUND);
+ searchBackground.setSpriteId(SpriteID.EQUIPMENT_SLOT_TILE);
+ }
+}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TabManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TabManager.java
new file mode 100644
index 0000000000..dd4308af04
--- /dev/null
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TabManager.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2018, Tomas Slusny
+ * Copyright (c) 2018, Ron Young
+ * 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.banktags.tabs;
+
+import com.google.common.base.MoreObjects;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import lombok.Getter;
+import net.runelite.api.ItemID;
+import net.runelite.client.config.ConfigManager;
+import static net.runelite.client.plugins.banktags.BankTagsPlugin.CONFIG_GROUP;
+import static net.runelite.client.plugins.banktags.BankTagsPlugin.ICON_SEARCH;
+import static net.runelite.client.plugins.banktags.BankTagsPlugin.JOINER;
+import static net.runelite.client.plugins.banktags.BankTagsPlugin.SPLITTER;
+import net.runelite.client.util.Text;
+import org.apache.commons.lang3.math.NumberUtils;
+
+@Singleton
+class TabManager
+{
+ private static final String TAG_TABS_CONFIG = "tagtabs";
+
+ @Getter
+ private final List tabs = new ArrayList<>();
+ private final ConfigManager configManager;
+
+ @Inject
+ private TabManager(ConfigManager configManager)
+ {
+ this.configManager = configManager;
+ }
+
+ void add(TagTab tagTab)
+ {
+ if (!contains(tagTab.getTag()))
+ {
+ tabs.add(tagTab);
+ }
+ }
+
+ void clear()
+ {
+ tabs.forEach(t -> t.setHidden(true));
+ tabs.clear();
+ }
+
+ TagTab find(String tag)
+ {
+ Optional first = tabs.stream().filter(t -> t.getTag().equals(Text.standardize(tag))).findAny();
+ return first.orElse(null);
+ }
+
+ List getAllTabs()
+ {
+ return SPLITTER.splitToList(MoreObjects.firstNonNull(configManager.getConfiguration(CONFIG_GROUP, TAG_TABS_CONFIG), ""));
+ }
+
+ TagTab load(String tag)
+ {
+ TagTab tagTab = find(tag);
+
+ if (tagTab == null)
+ {
+ tag = Text.standardize(tag);
+ String item = configManager.getConfiguration(CONFIG_GROUP, ICON_SEARCH + tag);
+ int itemid = NumberUtils.toInt(item, ItemID.SPADE);
+ tagTab = new TagTab(itemid, tag);
+ }
+
+ return tagTab;
+ }
+
+ void move(String tagToMove, String tagDestination)
+ {
+ tagToMove = Text.standardize(tagToMove);
+ tagDestination = Text.standardize(tagDestination);
+
+ if (contains(tagToMove) && contains(tagDestination))
+ {
+ Collections.swap(tabs, indexOf(tagToMove), indexOf(tagDestination));
+ }
+ }
+
+ void remove(String tag)
+ {
+ TagTab tagTab = find(tag);
+
+ if (tagTab != null)
+ {
+ tagTab.setHidden(true);
+ tabs.remove(tagTab);
+ }
+ }
+
+ void save()
+ {
+ String tags = JOINER.join(tabs.stream().map(TagTab::getTag).collect(Collectors.toList()));
+ configManager.setConfiguration(CONFIG_GROUP, TAG_TABS_CONFIG, tags);
+ }
+
+ int size()
+ {
+ return tabs.size();
+ }
+
+ private boolean contains(String tag)
+ {
+ return tabs.stream().anyMatch(t -> t.getTag().equals(tag));
+ }
+
+ private int indexOf(TagTab tagTab)
+ {
+ return tabs.indexOf(tagTab);
+ }
+
+ private int indexOf(String tag)
+ {
+ return indexOf(find(tag));
+ }
+}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TabSprites.java b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TabSprites.java
new file mode 100644
index 0000000000..290db8c67c
--- /dev/null
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TabSprites.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2018, Tomas Slusny
+ * Copyright (c) 2018, Ron Young
+ * 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.banktags.tabs;
+
+import java.awt.image.BufferedImage;
+import java.awt.image.PixelGrabber;
+import java.util.HashMap;
+import java.util.Map;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import net.runelite.api.Client;
+import net.runelite.api.SpritePixels;
+import net.runelite.client.util.ImageUtil;
+
+@Slf4j
+public enum TabSprites
+{
+ INCINERATOR(-200, "incinerator.png"),
+ TAB_BACKGROUND(-201, "tag-tab.png"),
+ TAB_BACKGROUND_ACTIVE(-202, "tag-tab-active.png"),
+ UP_ARROW(-203, "up-arrow.png"),
+ DOWN_ARROW(-204, "down-arrow.png"),
+ NEW_TAB(-205, "new-tab.png");
+
+ @Getter
+ private final int spriteId;
+ private final BufferedImage image;
+
+ TabSprites(final int spriteId, final String imageName)
+ {
+ this.spriteId = spriteId;
+ this.image = ImageUtil.getResourceStreamFromClass(this.getClass(), imageName);
+ }
+
+ public static Map toMap(Client client)
+ {
+ final Map map = new HashMap<>();
+
+ for (TabSprites value : values())
+ {
+ map.put(value.spriteId, getSpritePixels(client, value.image));
+ }
+
+ return map;
+ }
+
+ private static SpritePixels getSpritePixels(Client client, BufferedImage image)
+ {
+ int[] pixels = new int[image.getWidth() * image.getHeight()];
+
+ try
+ {
+ new PixelGrabber(image, 0, 0, image.getWidth(), image.getHeight(), pixels, 0, image.getWidth())
+ .grabPixels();
+ }
+ catch (InterruptedException ex)
+ {
+ log.debug("PixelGrabber was interrupted: ", ex);
+ }
+
+ return client.createSpritePixels(pixels, image.getWidth(), image.getHeight());
+ }
+}
diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TagTab.java b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TagTab.java
new file mode 100644
index 0000000000..d3d417f465
--- /dev/null
+++ b/runelite-client/src/main/java/net/runelite/client/plugins/banktags/tabs/TagTab.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2018, Tomas Slusny
+ * Copyright (c) 2018, Ron Young
+ * 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.banktags.tabs;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import net.runelite.api.widgets.Widget;
+
+@Data
+@EqualsAndHashCode(of = "tag")
+class TagTab
+{
+ private final String tag;
+ private int iconItemId;
+ private Widget background;
+ private Widget icon;
+
+ TagTab(int iconItemId, String tag)
+ {
+ this.iconItemId = iconItemId;
+ this.tag = tag;
+ }
+
+ void setHidden(boolean hide)
+ {
+ if (background != null)
+ {
+ background.setHidden(hide);
+ }
+
+ if (icon != null)
+ {
+ icon.setHidden(hide);
+ }
+ }
+}
diff --git a/runelite-client/src/main/java/net/runelite/client/util/Text.java b/runelite-client/src/main/java/net/runelite/client/util/Text.java
index 6b7f953d2e..dd40bad1d0 100644
--- a/runelite-client/src/main/java/net/runelite/client/util/Text.java
+++ b/runelite-client/src/main/java/net/runelite/client/util/Text.java
@@ -45,4 +45,13 @@ public class Text
return TAG_REGEXP.matcher(str).replaceAll("");
}
+ /**
+ * In addition to removing all tags, replaces nbsp with space, trims string and lowercases it
+ * @param str The string to standardize
+ * @return The given `str` that is standardized
+ */
+ public static String standardize(String str)
+ {
+ return Text.removeTags(str).replace('\u00A0', ' ').trim().toLowerCase();
+ }
}
diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/banktags/tabs/down-arrow.png b/runelite-client/src/main/resources/net/runelite/client/plugins/banktags/tabs/down-arrow.png
new file mode 100644
index 0000000000..b9f98803c0
Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/banktags/tabs/down-arrow.png differ
diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/banktags/tabs/incinerator.png b/runelite-client/src/main/resources/net/runelite/client/plugins/banktags/tabs/incinerator.png
new file mode 100644
index 0000000000..87047a0782
Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/banktags/tabs/incinerator.png differ
diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/banktags/tabs/new-tab.png b/runelite-client/src/main/resources/net/runelite/client/plugins/banktags/tabs/new-tab.png
new file mode 100644
index 0000000000..d2a1a0c8cf
Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/banktags/tabs/new-tab.png differ
diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/banktags/tabs/tag-tab-active.png b/runelite-client/src/main/resources/net/runelite/client/plugins/banktags/tabs/tag-tab-active.png
new file mode 100644
index 0000000000..7e95f0f09a
Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/banktags/tabs/tag-tab-active.png differ
diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/banktags/tabs/tag-tab.png b/runelite-client/src/main/resources/net/runelite/client/plugins/banktags/tabs/tag-tab.png
new file mode 100644
index 0000000000..815f15f3b6
Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/banktags/tabs/tag-tab.png differ
diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/banktags/tabs/up-arrow.png b/runelite-client/src/main/resources/net/runelite/client/plugins/banktags/tabs/up-arrow.png
new file mode 100644
index 0000000000..2b2ed9a4c3
Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/banktags/tabs/up-arrow.png differ
diff --git a/runelite-client/src/main/scripts/TriggerBankLayout.hash b/runelite-client/src/main/scripts/TriggerBankLayout.hash
new file mode 100644
index 0000000000..d4de8e0bbc
--- /dev/null
+++ b/runelite-client/src/main/scripts/TriggerBankLayout.hash
@@ -0,0 +1 @@
+A5E68A8B5B344D82FAF2CC11FE88D43A150A60094C93E6C52DC5CDCB0BF576D0
\ No newline at end of file
diff --git a/runelite-client/src/main/scripts/TriggerBankLayout.rs2asm b/runelite-client/src/main/scripts/TriggerBankLayout.rs2asm
new file mode 100644
index 0000000000..4b739a959c
--- /dev/null
+++ b/runelite-client/src/main/scripts/TriggerBankLayout.rs2asm
@@ -0,0 +1,42 @@
+.id 276
+.int_stack_count 13
+.string_stack_count 0
+.int_var_count 13
+.string_var_count 0
+
+; Check if we should allow server to relayout bank
+ load_int 1 ; true
+ load_int 0 ; load active boolean
+ load_string "getSearchingTagTab" ; push event name
+ runelite_callback ; invoke callback
+ if_icmpne LABEL2
+
+; Let layout continue if current bank tab is 0
+ get_varbit 4150
+ load_int 0
+ if_icmpeq LABEL2
+
+; Reset the current bank tab to 0 otherwise
+ load_int 0
+ set_varbit 4150
+
+ load_string "Server attempted to reset bank tab."
+ load_string "debug"
+ runelite_callback
+
+LABEL2:
+ iload 0
+ iload 1
+ iload 2
+ iload 3
+ iload 4
+ iload 5
+ iload 6
+ iload 7
+ iload 8
+ iload 9
+ iload 10
+ iload 11
+ iload 12
+ invoke 277
+ return