diff --git a/http-api/src/main/java/net/runelite/http/api/item/ItemClient.java b/http-api/src/main/java/net/runelite/http/api/item/ItemClient.java index f8c8ab2b5a..73bf93fbd3 100644 --- a/http-api/src/main/java/net/runelite/http/api/item/ItemClient.java +++ b/http-api/src/main/java/net/runelite/http/api/item/ItemClient.java @@ -25,9 +25,11 @@ package net.runelite.http.api.item; import com.google.gson.JsonParseException; +import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import javax.imageio.ImageIO; import net.runelite.http.api.RuneLiteAPI; import okhttp3.HttpUrl; import okhttp3.Request; @@ -70,6 +72,34 @@ public class ItemClient } } + public BufferedImage getIcon(int itemId) throws IOException + { + HttpUrl url = RuneLiteAPI.getApiBase().newBuilder() + .addPathSegment("item") + .addPathSegment("" + itemId) + .addPathSegment("icon") + .build(); + + logger.debug("Built URI: {}", url); + + Request request = new Request.Builder() + .url(url) + .build(); + + try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute()) + { + if (!response.isSuccessful()) + { + logger.debug("Error grabbing icon {}: {}", itemId, response.message()); + return null; + } + + InputStream in = response.body().byteStream(); + BufferedImage imageIcon = ImageIO.read(in); + return imageIcon; + } + } + public SearchResult search(String itemName) throws IOException { HttpUrl url = RuneLiteAPI.getApiBase().newBuilder() diff --git a/http-service/src/main/java/net/runelite/http/service/item/ItemController.java b/http-service/src/main/java/net/runelite/http/service/item/ItemController.java index cb157b2309..f63407463f 100644 --- a/http-service/src/main/java/net/runelite/http/service/item/ItemController.java +++ b/http-service/src/main/java/net/runelite/http/service/item/ItemController.java @@ -85,43 +85,29 @@ public class ItemController } @RequestMapping(path = "/{itemId}/icon", produces = "image/gif") - public byte[] getIcon(HttpServletResponse response, @PathVariable int itemId) + public ResponseEntity getIcon(@PathVariable int itemId) { ItemEntry item = itemService.getItem(itemId); if (item != null && item.getIcon() != null) { - response.setHeader(RUNELITE_CACHE, "HIT"); - return item.getIcon(); + return ResponseEntity.ok(item.getIcon()); } - item = itemService.fetchItem(itemId); - if (item != null) - { - response.setHeader(RUNELITE_CACHE, "MISS"); - return item.getIcon(); - } - - return null; + itemService.queueItem(itemId); + return ResponseEntity.notFound().build(); } @RequestMapping(path = "/{itemId}/icon/large", produces = "image/gif") - public byte[] getIconLarge(HttpServletResponse response, @PathVariable int itemId) + public ResponseEntity getIconLarge(HttpServletResponse response, @PathVariable int itemId) { ItemEntry item = itemService.getItem(itemId); if (item != null && item.getIcon_large() != null) { - response.setHeader(RUNELITE_CACHE, "HIT"); - return item.getIcon_large(); + return ResponseEntity.ok(item.getIcon_large()); } - item = itemService.fetchItem(itemId); - if (item != null) - { - response.setHeader(RUNELITE_CACHE, "MISS"); - return item.getIcon_large(); - } - - return null; + itemService.queueItem(itemId); + return ResponseEntity.notFound().build(); } @RequestMapping("/{itemId}/price") @@ -195,7 +181,7 @@ public class ItemController if (priceEntry.getFetched_time().isBefore(cacheTime)) { // Queue a check for the price - itemService.queueLookup(itemId); + itemService.queuePriceLookup(itemId); } } diff --git a/http-service/src/main/java/net/runelite/http/service/item/ItemService.java b/http-service/src/main/java/net/runelite/http/service/item/ItemService.java index c877579acb..9f05e049eb 100644 --- a/http-service/src/main/java/net/runelite/http/service/item/ItemService.java +++ b/http-service/src/main/java/net/runelite/http/service/item/ItemService.java @@ -86,6 +86,7 @@ public class ItemService private final Sql2o sql2o; private final ConcurrentLinkedQueue pendingPriceLookups = new ConcurrentLinkedQueue<>(); private final ConcurrentLinkedQueue pendingSearches = new ConcurrentLinkedQueue<>(); + private final ConcurrentLinkedQueue pendingItems = new ConcurrentLinkedQueue<>(); @Autowired public ItemService(@Qualifier("Runelite SQL2O") Sql2o sql2o) @@ -362,7 +363,7 @@ public class ItemService } } - public void queueLookup(int itemId) + public void queuePriceLookup(int itemId) { if (pendingPriceLookups.size() >= MAX_PENDING) { @@ -382,25 +383,48 @@ public class ItemService pendingSearches.add(search); } - @Scheduled(fixedDelay = 5000) - public void checkPrices() + public void queueItem(int itemId) { - Integer itemId = pendingPriceLookups.poll(); - if (itemId == null) + if (pendingItems.size() >= MAX_PENDING) { return; } - fetchPrice(itemId); + pendingItems.add(itemId); } @Scheduled(fixedDelay = 5000) - public void checkSearches() + public void check() + { + if (checkPrices()) + { + return; + } + if (checkSearches()) + { + return; + } + checkItems(); + } + + private boolean checkPrices() + { + Integer itemId = pendingPriceLookups.poll(); + if (itemId == null) + { + return false; + } + + fetchPrice(itemId); + return true; + } + + private boolean checkSearches() { String search = pendingSearches.poll(); if (search == null) { - return; + return false; } try @@ -413,5 +437,19 @@ public class ItemService { log.warn("error while searching items", ex); } + + return true; + } + + private boolean checkItems() + { + Integer itemId = pendingItems.poll(); + if (itemId == null) + { + return false; + } + + fetchItem(itemId); + return true; } } diff --git a/runelite-client/src/main/java/net/runelite/client/input/KeyManager.java b/runelite-client/src/main/java/net/runelite/client/input/KeyManager.java index d4bfb6a8db..d8e5c81a11 100644 --- a/runelite-client/src/main/java/net/runelite/client/input/KeyManager.java +++ b/runelite-client/src/main/java/net/runelite/client/input/KeyManager.java @@ -36,7 +36,10 @@ public class KeyManager public void registerKeyListener(KeyListener keyListener) { - keyListeners.add(keyListener); + if (!keyListeners.contains(keyListener)) + { + keyListeners.add(keyListener); + } } public void unregisterKeyListener(KeyListener keyListener) diff --git a/runelite-client/src/main/java/net/runelite/client/input/MouseManager.java b/runelite-client/src/main/java/net/runelite/client/input/MouseManager.java index 41656664f0..dbb8bd3695 100644 --- a/runelite-client/src/main/java/net/runelite/client/input/MouseManager.java +++ b/runelite-client/src/main/java/net/runelite/client/input/MouseManager.java @@ -38,7 +38,10 @@ public class MouseManager public void registerMouseListener(MouseListener mouseListener) { - mouseListeners.add(mouseListener); + if (!mouseListeners.contains(mouseListener)) + { + mouseListeners.add(mouseListener); + } } public void unregisterMouseListener(MouseListener mouseListener) @@ -48,7 +51,10 @@ public class MouseManager public void registerMouseWheelListener(MouseWheelListener mouseWheelListener) { - mouseWheelListeners.add(mouseWheelListener); + if (!mouseWheelListeners.contains(mouseWheelListener)) + { + mouseWheelListeners.add(mouseWheelListener); + } } public void unregisterMouseWheelListener(MouseWheelListener mouseWheelListener) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeConfig.java new file mode 100644 index 0000000000..94d4e981c6 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeConfig.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2018, Seth + * 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.grandexchange; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup( + keyName = "grandexchange", + name = "Grand Exchange", + description = "Configuration for the Grand Exchange" +) +public interface GrandExchangeConfig extends Config +{ + @ConfigItem( + keyName = "quickLookup", + name = "Hotkey lookup (Alt + Left click)", + description = "Configures whether to enable the hotkey lookup for ge searches" + ) + default boolean quickLookup() + { + return true; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeItemPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeItemPanel.java new file mode 100644 index 0000000000..3d1b13c2fb --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeItemPanel.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2018, Seth + * 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.grandexchange; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Desktop; +import java.awt.GridLayout; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.text.NumberFormat; +import javax.swing.ImageIcon; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; +import javax.swing.border.LineBorder; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +class GrandExchangeItemPanel extends JPanel +{ + GrandExchangeItemPanel(BufferedImage icon, String name, int itemID, int gePrice, Double haPrice) + { + BorderLayout layout = new BorderLayout(); + layout.setHgap(5); + setLayout(layout); + setToolTipText(name); + + Color background = getBackground(); + + addMouseListener(new MouseAdapter() + { + @Override + public void mouseEntered(MouseEvent e) + { + setBackground(getBackground().darker().darker()); + } + + @Override + public void mouseExited(MouseEvent e) + { + setBackground(background); + } + + @Override + public void mouseReleased(MouseEvent e) + { + geLink(name, itemID); + } + }); + + setBorder(new CompoundBorder + ( + new LineBorder(getBackground().brighter(), 1), + new EmptyBorder(5, 5, 5, 5) + )); + + // Icon + JLabel itemIcon = new JLabel(); + itemIcon.setIcon(new ImageIcon(icon)); + add(itemIcon, BorderLayout.LINE_START); + + // Item details panel + JPanel rightPanel = new JPanel(new GridLayout(3, 1)); + rightPanel.setOpaque(false); + + // Item name + JLabel itemName = new JLabel(); + itemName.setText(name); + rightPanel.add(itemName); + + // Ge price + JLabel gePriceLabel = new JLabel(); + gePriceLabel.setText(NumberFormat.getInstance().format(gePrice) + " gp"); + gePriceLabel.setForeground(Color.GREEN); + rightPanel.add(gePriceLabel); + + // Alch price + JLabel haPriceLabel = new JLabel(); + haPriceLabel.setText(NumberFormat.getInstance().format(haPrice.intValue()) + " alch"); + haPriceLabel.setForeground(Color.orange); + rightPanel.add(haPriceLabel); + + add(rightPanel, BorderLayout.CENTER); + } + + public void geLink(String name, int itemID) + { + String url = "http://services.runescape.com/m=itemdb_oldschool/" + name.replaceAll(" ", "_") + "/viewitem?obj=" + itemID; + + if (!Desktop.isDesktopSupported()) + { + log.info("Desktop is not supported. Visit {}", url); + return; + } + + Desktop desktop = Desktop.getDesktop(); + if (!desktop.isSupported(Desktop.Action.BROWSE)) + { + log.info("Desktop browser is not supported. Visit {}", url); + return; + } + + try + { + desktop.browse(new URI(url)); + + log.debug("Opened browser to {}", url); + } + catch (IOException | URISyntaxException ex) + { + log.warn("Unable to open grand exchange page", ex); + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeItems.java b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeItems.java new file mode 100644 index 0000000000..28300b295a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeItems.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2018, Seth + * 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.grandexchange; + +import java.awt.image.BufferedImage; +import lombok.Data; + +@Data +public class GrandExchangeItems +{ + private final BufferedImage icon; + private final String name; + private final int itemId; + private final int gePrice; + private final double haPrice; + + GrandExchangeItems(BufferedImage icon, String name, int itemId, int gePrice, double haPrice) + { + this.icon = icon; + this.name = name; + this.itemId = itemId; + this.gePrice = gePrice; + this.haPrice = haPrice; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePanel.java index c7db78ab89..1c956029cf 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePanel.java @@ -25,9 +25,15 @@ package net.runelite.client.plugins.grandexchange; +import java.awt.BorderLayout; +import java.util.concurrent.ScheduledExecutorService; import javax.inject.Inject; import javax.swing.BoxLayout; +import javax.swing.JPanel; +import javax.swing.JTabbedPane; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; import net.runelite.api.GrandExchangeOffer; import net.runelite.client.game.ItemManager; import net.runelite.client.ui.PluginPanel; @@ -37,20 +43,34 @@ class GrandExchangePanel extends PluginPanel { private static final int MAX_OFFERS = 8; + @Getter + private GrandExchangeSearchPanel searchPanel; + private GrandExchangeOfferSlot[] offerSlotPanels = new GrandExchangeOfferSlot[MAX_OFFERS]; - @Inject - GrandExchangePanel(ItemManager itemManager) - { - setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + private JPanel offerPanel = new JPanel(); + private JTabbedPane tabbedPane = new JTabbedPane(); + + @Inject + GrandExchangePanel(Client client, ItemManager itemManager, ScheduledExecutorService executor) + { + setLayout(new BorderLayout()); + add(tabbedPane, BorderLayout.NORTH); + + // Offer Panel + offerPanel.setLayout(new BoxLayout(offerPanel, BoxLayout.Y_AXIS)); for (int i = 0; i < offerSlotPanels.length; ++i) { offerSlotPanels[i] = new GrandExchangeOfferSlot(itemManager); - add(offerSlotPanels[i]); + offerPanel.add(offerSlotPanels[i]); } - setVisible(true); + // Search Panel + searchPanel = new GrandExchangeSearchPanel(client, itemManager, executor); + + tabbedPane.addTab("Offers", offerPanel); + tabbedPane.addTab("Search", searchPanel); } void updateOffer(GrandExchangeOffer newOffer, int slot) @@ -58,4 +78,14 @@ class GrandExchangePanel extends PluginPanel offerSlotPanels[slot].updateOffer(newOffer); } + void showSearch() + { + if (searchPanel.isShowing()) + { + return; + } + + tabbedPane.setSelectedComponent(searchPanel); + revalidate(); + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePlugin.java index 9e1cd336b9..f9b447e03a 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePlugin.java @@ -28,12 +28,24 @@ package net.runelite.client.plugins.grandexchange; import com.google.common.eventbus.Subscribe; +import com.google.inject.Provides; +import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.io.IOException; import javax.imageio.ImageIO; import javax.inject.Inject; import javax.swing.SwingUtilities; +import net.runelite.api.Client; +import net.runelite.api.ItemComposition; +import net.runelite.api.Point; +import net.runelite.api.events.ConfigChanged; import net.runelite.api.events.GrandExchangeOfferChanged; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetInfo; +import net.runelite.api.widgets.WidgetItem; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.input.MouseListener; +import net.runelite.client.input.MouseManager; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; import net.runelite.client.ui.ClientUI; @@ -48,9 +60,26 @@ public class GrandExchangePlugin extends Plugin private GrandExchangePanel panel; + private MouseListener itemClick; + + @Inject + private MouseManager mouseManager; + + @Inject + private Client client; + @Inject private ClientUI ui; + @Inject + private GrandExchangeConfig config; + + @Provides + GrandExchangeConfig provideConfig(ConfigManager configManager) + { + return configManager.getConfig(GrandExchangeConfig.class); + } + @Override protected void startUp() throws IOException { @@ -58,12 +87,82 @@ public class GrandExchangePlugin extends Plugin BufferedImage icon = ImageIO.read(getClass().getResourceAsStream("ge_icon.png")); button = new NavigationButton("GE Offers", icon, () -> panel); ui.getPluginToolbar().addNavigation(button); + + itemClick = new MouseListener() + { + @Override + public MouseEvent mouseClicked(MouseEvent e) + { + // Check if left click + alt + if (e.getButton() == MouseEvent.BUTTON1 && e.isAltDown()) + { + Point mousePosition = client.getMouseCanvasPosition(); + Widget inventoryWidget = client.getWidget(WidgetInfo.INVENTORY); + if (inventoryWidget != null && !inventoryWidget.isHidden()) + { + for (WidgetItem item : inventoryWidget.getWidgetItems()) + { + if (item.getCanvasBounds().contains(mousePosition.getX(), mousePosition.getY())) + { + ItemComposition itemComp = client.getItemDefinition(item.getId()); + if (itemComp != null) + { + e.consume(); + + SwingUtilities.invokeLater(() -> + { + panel.showSearch(); + + if (!button.isSelected()) + { + button.doClick(); + } + + panel.getSearchPanel().priceLookup(itemComp.getName()); + }); + } + + break; + } + } + } + } + + return super.mouseClicked(e); + } + }; + + if (config.quickLookup()) + { + mouseManager.registerMouseListener(itemClick); + } } @Override protected void shutDown() { ui.getPluginToolbar().removeNavigation(button); + + mouseManager.unregisterMouseListener(itemClick); + } + + @Subscribe + public void onConfigChange(ConfigChanged event) + { + if (event.getGroup().equals("grandexchange")) + { + if (event.getKey().equals("quickLookup")) + { + if (config.quickLookup()) + { + mouseManager.registerMouseListener(itemClick); + } + else + { + mouseManager.unregisterMouseListener(itemClick); + } + } + } } @Subscribe diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeSearchPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeSearchPanel.java new file mode 100644 index 0000000000..3a6ac59448 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeSearchPanel.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2018, Seth + * 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.grandexchange; + +import com.google.common.base.Strings; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.GridLayout; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import javax.imageio.ImageIO; +import javax.swing.Icon; +import javax.swing.ImageIcon; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import javax.swing.border.EmptyBorder; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.ItemComposition; +import net.runelite.client.game.ItemManager; +import net.runelite.client.plugins.hiscore.IconTextField; +import net.runelite.http.api.item.Item; +import net.runelite.http.api.item.ItemClient; +import net.runelite.http.api.item.ItemPrice; +import net.runelite.http.api.item.SearchResult; + +@Slf4j +class GrandExchangeSearchPanel extends JPanel +{ + private static final List ITEMS_LIST = new ArrayList<>(); + + private final Client client; + private final ItemManager itemManager; + private final ScheduledExecutorService executor; + + private ItemClient itemClient; + + private Icon search; + + private IconTextField searchBox = new IconTextField(); + private JPanel container = new JPanel(); + private JPanel searchItemsPanel = new JPanel(); + private JLabel searchingLabel = new JLabel("Searching..."); + + GrandExchangeSearchPanel(Client client, ItemManager itemManager, ScheduledExecutorService executor) + { + this.client = client; + this.itemManager = itemManager; + this.executor = executor; + init(); + } + + void init() + { + setLayout(new BorderLayout()); + container.setLayout(new BorderLayout()); + + // Search Box + try + { + search = new ImageIcon(ImageIO.read(GrandExchangePlugin.class.getResourceAsStream("search.png"))); + } + catch (IOException e) + { + log.warn("Failed to read icon", e); + } + + searchBox.setIcon(search); + searchBox.addActionListener(e -> executor.execute(() -> priceLookup(false))); + + container.add(searchBox, BorderLayout.NORTH); + + // Searching label + searchingLabel.setHorizontalAlignment(JLabel.CENTER); + searchingLabel.setForeground(Color.YELLOW); + + // Items Panel + searchItemsPanel.setLayout(new GridLayout(0, 1, 0, 3)); + searchItemsPanel.setBorder(new EmptyBorder(3, 0, 0, 0)); + + container.add(searchItemsPanel, BorderLayout.SOUTH); + add(container, BorderLayout.NORTH); + } + + void priceLookup(String item) + { + searchBox.setText(item); + executor.execute(() -> priceLookup(true)); + } + + private void priceLookup(boolean exactMatch) + { + String lookup = searchBox.getText(); + + if (Strings.isNullOrEmpty(lookup)) + { + return; + } + + // Input is not empty, add searching label + searchItemsPanel.removeAll(); + showSearchString(true); + + SearchResult result; + + try + { + result = itemManager.searchForItem(lookup); + } + catch (ExecutionException ex) + { + log.warn("Unable to search for item {}", lookup, ex); + showSearchString(false); + return; + } + + + if (result != null && !result.getItems().isEmpty()) + { + itemClient = new ItemClient(); + + for (Item item : result.getItems()) + { + int itemId = item.getId(); + + ItemComposition itemComp = client.getItemDefinition(itemId); + if (itemComp == null) + { + continue; + } + + ItemPrice itemPrice; + try + { + itemPrice = itemManager.getItemPrice(itemId); + } + catch (IOException ex) + { + log.warn("Unable to fetch item price for {}", itemId, ex); + showSearchString(false); + return; + } + + BufferedImage itemImage; + try + { + itemImage = itemClient.getIcon(itemId); + } + catch (IOException ex) + { + log.warn("Unable to fetch item icon for {}", itemId, ex); + showSearchString(false); + return; + } + + if (itemImage == null) + { + log.warn("Unable to fetch item icon for {}", itemId); + showSearchString(false); + return; + } + + ITEMS_LIST.add(new GrandExchangeItems(itemImage, item.getName(), itemId, itemPrice.getPrice(), itemComp.getPrice() * 0.6)); + + // If using hotkey to lookup item, stop after finding match. + if (exactMatch && item.getName().equalsIgnoreCase(lookup)) + { + break; + } + } + } + + SwingUtilities.invokeLater(() -> + { + for (GrandExchangeItems item : ITEMS_LIST) + { + GrandExchangeItemPanel panel = new GrandExchangeItemPanel(item.getIcon(), item.getName(), item.getItemId(), item.getGePrice(), item.getHaPrice()); + + searchItemsPanel.add(panel); + } + + ITEMS_LIST.clear(); + + // Remove searching label after search is complete + showSearchString(false); + }); + } + + private void showSearchString(boolean shown) + { + if (shown) + { + add(searchingLabel, BorderLayout.CENTER); + } + else + { + remove(searchingLabel); + } + + revalidate(); + repaint(); + } +} diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/grandexchange/search.png b/runelite-client/src/main/resources/net/runelite/client/plugins/grandexchange/search.png new file mode 100644 index 0000000000..23bf66b759 Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/grandexchange/search.png differ