diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeOfferSlot.java b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeOfferSlot.java new file mode 100644 index 0000000000..ac3bde95cf --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeOfferSlot.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2018, SomeoneWithAnInternetConnection + * 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.CardLayout; +import java.awt.Color; +import javax.annotation.Nullable; +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.ImageIcon; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JProgressBar; +import javax.swing.border.TitledBorder; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.GrandExchangeOffer; +import net.runelite.api.GrandExchangeOfferState; +import static net.runelite.api.GrandExchangeOfferState.EMPTY; +import net.runelite.api.ItemComposition; +import net.runelite.client.game.ItemManager; + +@Slf4j +public class GrandExchangeOfferSlot extends JPanel +{ + private static final Color GE_INPROGRESS_ORANGE = new Color(0xd8, 0x80, 0x20).brighter(); + private static final Color GE_FINISHED_GREEN = new Color(0, 0x5f, 0); + private static final Color GE_CANCELLED_RED = new Color(0x8f, 0, 0); + + private static final String INFO_CARD = "INFO_CARD"; + private static final String EMPTY_CARD = "EMPTY_CARD"; + + private final ItemManager itemManager; + + private final CardLayout cardLayout = new CardLayout(); + private final JLabel itemIcon = new JLabel(); + private final TitledBorder itemName = BorderFactory.createTitledBorder("Nothing"); + private final JLabel offerState = new JLabel("Text so the label has height"); + private final JProgressBar progressBar = new JProgressBar(); + + /** + * This (sub)panel is used for each GE slot displayed + * in the sidebar + */ + GrandExchangeOfferSlot(ItemManager itemManager) + { + this.itemManager = itemManager; + buildPanel(); + } + + private void buildPanel() + { + setBorder(BorderFactory.createCompoundBorder( + // Add a margin underneath each slot panel to space them out + BorderFactory.createEmptyBorder(0, 0, 3, 0), + itemName + )); + + // The default border color is kind of dark, so we change it to something lighter + itemName.setBorder(BorderFactory.createLineBorder(getBackground().brighter())); + + progressBar.setStringPainted(true); + + setLayout(cardLayout); + + // Card for when the slot has an offer in it + JPanel infoCard = new JPanel(); + add(infoCard, INFO_CARD); + // Add padding to give the icon and progress bar room to breathe + infoCard.setBorder(BorderFactory.createEmptyBorder(0, 2, 2, 2)); + + infoCard.setLayout(new BoxLayout(infoCard, BoxLayout.X_AXIS)); + // Icon on the left + infoCard.add(itemIcon); + + // Info on the right + JPanel offerStatePanel = new JPanel(); + offerStatePanel.setLayout(new BoxLayout(offerStatePanel, BoxLayout.Y_AXIS)); + offerStatePanel.add(offerState); + offerStatePanel.add(progressBar); + infoCard.add(offerStatePanel); + + // Card for when the slot is empty + JPanel emptySlotCard = new JPanel(); + add(emptySlotCard, EMPTY_CARD); + // Counteract the height lost to the text at the top of the TitledBorder + int itemNameBorderHeight = itemName.getBorderInsets(this).top; + emptySlotCard.setBorder(BorderFactory.createEmptyBorder(0, 0, (itemNameBorderHeight - 1) / 2, 0)); + // Center the "Empty" label horizontally + emptySlotCard.setLayout( new BoxLayout(emptySlotCard, BoxLayout.X_AXIS)); + emptySlotCard.add(Box.createHorizontalGlue()); + emptySlotCard.add(new JLabel(getNameForState(EMPTY)), BorderLayout.CENTER); + emptySlotCard.add(Box.createHorizontalGlue()); + + cardLayout.show(this, EMPTY_CARD); + + + + } + + void updateOffer(@Nullable GrandExchangeOffer newOffer) + { + if (newOffer == null || newOffer.getState() == EMPTY) + { + cardLayout.show(this, EMPTY_CARD); + itemName.setTitle("Nothing"); + } + else + { + cardLayout.show(this, INFO_CARD); + + ItemComposition offerItem = itemManager.getItemComposition(newOffer.getItemId()); + + itemName.setTitle(offerItem.getName()); + + boolean shouldStack = offerItem.isStackable() || newOffer.getTotalQuantity() > 1; + ImageIcon newItemIcon = new ImageIcon(itemManager.getImage(newOffer.getItemId(), newOffer.getTotalQuantity(), shouldStack)); + itemIcon.setIcon(newItemIcon); + + offerState.setText(getNameForState(newOffer.getState()) + " at " + newOffer.getPrice() + (newOffer.getTotalQuantity() > 1 ? "gp ea" : "gp")); + + progressBar.setMaximum(newOffer.getTotalQuantity()); + progressBar.setValue(newOffer.getQuantitySold()); + progressBar.setBackground(getColorForState(newOffer.getState())); + progressBar.setString(newOffer.getQuantitySold() + "/" + newOffer.getTotalQuantity()); + } + repaint(); + } + + private String getNameForState(GrandExchangeOfferState state) + { + switch (state) + { + case CANCELLED: + return "Cancelled"; + case BUYING: + return "Buying"; + case BOUGHT: + return "Bought"; + case SELLING: + return "Selling"; + case SOLD: + return "Sold"; + case EMPTY: + default: + return "Empty"; + + } + } + + private Color getColorForState(GrandExchangeOfferState state) + { + switch (state) + { + case CANCELLED: + return GE_CANCELLED_RED; + case BUYING: + case SELLING: + return GE_INPROGRESS_ORANGE; + case BOUGHT: + case SOLD: + return GE_FINISHED_GREEN; + case EMPTY: + default: + return null; + } + } + +} + + 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 new file mode 100644 index 0000000000..c7db78ab89 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePanel.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2018, SomeoneWithAnInternetConnection + * 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 javax.inject.Inject; +import javax.swing.BoxLayout; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.GrandExchangeOffer; +import net.runelite.client.game.ItemManager; +import net.runelite.client.ui.PluginPanel; + +@Slf4j +class GrandExchangePanel extends PluginPanel +{ + private static final int MAX_OFFERS = 8; + + private GrandExchangeOfferSlot[] offerSlotPanels = new GrandExchangeOfferSlot[MAX_OFFERS]; + + @Inject + GrandExchangePanel(ItemManager itemManager) + { + setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + + for (int i = 0; i < offerSlotPanels.length; ++i) + { + offerSlotPanels[i] = new GrandExchangeOfferSlot(itemManager); + add(offerSlotPanels[i]); + } + + setVisible(true); + } + + void updateOffer(GrandExchangeOffer newOffer, int slot) + { + offerSlotPanels[slot].updateOffer(newOffer); + } + +} 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 new file mode 100644 index 0000000000..9e1cd336b9 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangePlugin.java @@ -0,0 +1,78 @@ +/* + * + * Copyright (c) 2017, Robbie + * Copyright (c) 2018, SomeoneWithAnInternetConnection + * 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.eventbus.Subscribe; +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.events.GrandExchangeOfferChanged; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.ui.ClientUI; +import net.runelite.client.ui.NavigationButton; + +@PluginDescriptor( + name = "Grand Exchange offers" +) +public class GrandExchangePlugin extends Plugin +{ + private NavigationButton button; + + private GrandExchangePanel panel; + + @Inject + private ClientUI ui; + + @Override + protected void startUp() throws IOException + { + panel = injector.getInstance(GrandExchangePanel.class); + BufferedImage icon = ImageIO.read(getClass().getResourceAsStream("ge_icon.png")); + button = new NavigationButton("GE Offers", icon, () -> panel); + ui.getPluginToolbar().addNavigation(button); + } + + @Override + protected void shutDown() + { + ui.getPluginToolbar().removeNavigation(button); + } + + @Subscribe + public void onGrandExchangeOfferChanged(GrandExchangeOfferChanged offerEvent) + { + SwingUtilities.invokeLater(() -> + { + panel.updateOffer(offerEvent.getOffer(), offerEvent.getSlot()); + }); + } + +} diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/grandexchange/ge_icon.png b/runelite-client/src/main/resources/net/runelite/client/plugins/grandexchange/ge_icon.png new file mode 100644 index 0000000000..0783e0cd32 Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/grandexchange/ge_icon.png differ diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/grandexchange/GrandExchangeOfferSlotTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/grandexchange/GrandExchangeOfferSlotTest.java new file mode 100644 index 0000000000..99b0753c9e --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/plugins/grandexchange/GrandExchangeOfferSlotTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2018, SomeoneWithAnInternetConnection + * 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 net.runelite.api.GrandExchangeOffer; +import net.runelite.api.GrandExchangeOfferState; +import net.runelite.api.ItemComposition; +import net.runelite.client.game.ItemManager; +import org.junit.Test; +import org.junit.runner.RunWith; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyInt; +import org.mockito.Mock; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class GrandExchangeOfferSlotTest +{ + @Mock + private ItemManager itemManager; + + @Mock + private GrandExchangeOffer offer; + + @Test + public void testUpdateOffer() + { + when(itemManager.getItemComposition(anyInt())).thenReturn(mock(ItemComposition.class)); + when(itemManager.getImage(anyInt(), anyInt(), anyBoolean())).thenReturn(mock(BufferedImage.class)); + when(offer.getState()).thenReturn(GrandExchangeOfferState.CANCELLED); + + GrandExchangeOfferSlot offerSlot = new GrandExchangeOfferSlot(itemManager); + offerSlot.updateOffer(offer); + } + +}