From ab7e969320bc392e2b734af147c5ff5a18b42f4c Mon Sep 17 00:00:00 2001 From: Ruben Amendoeira Date: Sun, 22 Apr 2018 04:24:46 +0100 Subject: [PATCH] Grand Exchange Plugin redesign General: - Applied the design I proposed in issue #1342 - Applied custom component: MaterialTabs - Removed default scrolling behaviour from parent PluginPanel - Added error panels for empty searches and empty offer slots - Added new formatter to the StackFormatter that displays integers as rs stacks with decimals (21700 into 21.7k) - Changed the Locale on the stack formatter and respective unit testing to UK, this makes sure all tests are consistent with Travis (ex: i ran the unit testing in europe, travis ran in the us, so it passed my tests, failed his) Offers: - Refactored the GE offers into it's own seperate file: GrandExchangeOffersPanel - Redesigned the ge offers items - Included the custom component ThinProgressBar on the bottom of each ge item panel - Added secondary information panel, toggled by clicking on the primary panel - Added a game state check that resets all ge offers on logout Search: - Recoloured and resized the search bar - Added new icons to the search bar (incluing a loading wheel gif) - Removed focus on the search bar when results are displayed - Added custom scrolling behaviour - Blocked input when search is in progress --- .../grandexchange/GrandExchangeItemPanel.java | 28 +- .../grandexchange/GrandExchangeOfferSlot.java | 301 ++++++++++++------ .../GrandExchangeOffersPanel.java | 201 ++++++++++++ .../grandexchange/GrandExchangePanel.java | 59 ++-- .../grandexchange/GrandExchangePlugin.java | 12 +- .../GrandExchangeSearchPanel.java | 214 +++++++++---- .../ui/components/PluginErrorPanel.java | 2 +- .../runelite/client/util/StackFormatter.java | 63 +++- .../net/runelite/client/util/SwingUtil.java | 8 + .../plugins/grandexchange/arrow_left.png | Bin 0 -> 15918 bytes .../plugins/grandexchange/arrow_right.png | Bin 0 -> 15917 bytes .../client/plugins/grandexchange/search.png | Bin 388 -> 0 bytes .../client/util/StackFormatterTest.java | 27 ++ 13 files changed, 699 insertions(+), 216 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeOffersPanel.java create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/grandexchange/arrow_left.png create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/grandexchange/arrow_right.png delete mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/grandexchange/search.png 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 index 24905da2c4..0a12675637 100644 --- 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2018, Seth + * Copyright (c) 2018, Psikoi * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -32,14 +33,17 @@ import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; 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; +import net.runelite.client.ui.ColorScheme; import net.runelite.client.game.AsyncBufferedImage; import net.runelite.client.util.LinkBrowser; import net.runelite.client.util.StackFormatter; +/** + * This panel displays an individual item result in the + * Grand Exchange search plugin. + */ @Slf4j class GrandExchangeItemPanel extends JPanel { @@ -52,7 +56,8 @@ class GrandExchangeItemPanel extends JPanel layout.setHgap(5); setLayout(layout); setToolTipText(name); - + setBackground(ColorScheme.MEDIUM_GRAY_COLOR); + Color background = getBackground(); addMouseListener(new MouseAdapter() @@ -60,7 +65,7 @@ class GrandExchangeItemPanel extends JPanel @Override public void mouseEntered(MouseEvent e) { - setBackground(getBackground().darker().darker()); + setBackground(getBackground().brighter()); } @Override @@ -76,11 +81,7 @@ class GrandExchangeItemPanel extends JPanel } }); - setBorder(new CompoundBorder - ( - new LineBorder(getBackground().brighter(), 1), - new EmptyBorder(5, 5, 5, 5) - )); + setBorder(new EmptyBorder(5, 5, 5, 0)); // Icon JLabel itemIcon = new JLabel(); @@ -97,6 +98,9 @@ class GrandExchangeItemPanel extends JPanel // Item name JLabel itemName = new JLabel(); + itemName.setForeground(Color.WHITE); + itemName.setMaximumSize(new Dimension(0, 0)); // to limit the label's size for + itemName.setPreferredSize(new Dimension(0, 0)); // items with longer names itemName.setText(name); rightPanel.add(itemName); @@ -110,13 +114,13 @@ class GrandExchangeItemPanel extends JPanel { gePriceLabel.setText("N/A"); } - gePriceLabel.setForeground(Color.GREEN); + gePriceLabel.setForeground(ColorScheme.GRAND_EXCHANGE_PRICE); rightPanel.add(gePriceLabel); // Alch price JLabel haPriceLabel = new JLabel(); haPriceLabel.setText(StackFormatter.formatNumber(haPrice.intValue()) + " alch"); - haPriceLabel.setForeground(Color.orange); + haPriceLabel.setForeground(ColorScheme.GRAND_EXCHANGE_ALCH); rightPanel.add(haPriceLabel); add(rightPanel, BorderLayout.CENTER); @@ -131,4 +135,4 @@ class GrandExchangeItemPanel extends JPanel LinkBrowser.browse(url); } -} +} \ No newline at end of file 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 index 7e9409a26d..eab0023d68 100644 --- 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2018, SomeoneWithAnInternetConnection + * Copyright (c) 2018, Psikoi * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -28,38 +29,73 @@ package net.runelite.client.plugins.grandexchange; import java.awt.BorderLayout; import java.awt.CardLayout; import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.GridLayout; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; import java.awt.image.BufferedImage; +import java.io.IOException; import javax.annotation.Nullable; -import javax.swing.BorderFactory; -import javax.swing.Box; -import javax.swing.BoxLayout; +import javax.imageio.ImageIO; import javax.swing.ImageIcon; import javax.swing.JLabel; import javax.swing.JPanel; -import javax.swing.JProgressBar; -import javax.swing.border.TitledBorder; +import javax.swing.border.EmptyBorder; import lombok.extern.slf4j.Slf4j; import net.runelite.api.GrandExchangeOffer; import net.runelite.api.GrandExchangeOfferState; +import static net.runelite.api.GrandExchangeOfferState.CANCELLED_BUY; +import static net.runelite.api.GrandExchangeOfferState.CANCELLED_SELL; import static net.runelite.api.GrandExchangeOfferState.EMPTY; import net.runelite.api.ItemComposition; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.FontManager; +import net.runelite.client.ui.components.ThinProgressBar; import net.runelite.client.util.StackFormatter; +import net.runelite.client.util.SwingUtil; @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 FACE_CARD = "FACE_CARD"; + private static final String DETAILS_CARD = "DETAILS_CARD"; - private static final String INFO_CARD = "INFO_CARD"; - private static final String EMPTY_CARD = "EMPTY_CARD"; + private static final ImageIcon RIGHT_ARROW_ICON; + private static final ImageIcon LEFT_ARROW_ICON; + private final JPanel container = new JPanel(); 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(); + private final JLabel itemName = new JLabel(); + private final JLabel offerInfo = new JLabel(); + private final JLabel switchFaceViewIcon = new JLabel(); + + private final JLabel itemPrice = new JLabel(); + private final JLabel offerSpent = new JLabel(); + private final JLabel switchDetailsViewIcon = new JLabel(); + + private final ThinProgressBar progressBar = new ThinProgressBar(); + + private boolean showingFace = true; + + static + { + try + { + synchronized (ImageIO.class) + { + RIGHT_ARROW_ICON = new ImageIcon(ImageIO.read(GrandExchangeOfferSlot.class.getResourceAsStream("arrow_right.png"))); + LEFT_ARROW_ICON = new ImageIcon(ImageIO.read(GrandExchangeOfferSlot.class.getResourceAsStream("arrow_left.png"))); + } + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } /** * This (sub)panel is used for each GE slot displayed @@ -72,123 +108,188 @@ public class GrandExchangeOfferSlot extends JPanel private void buildPanel() { - setBorder(BorderFactory.createCompoundBorder( - // Add a margin underneath each slot panel to space them out - BorderFactory.createEmptyBorder(0, 0, 3, 0), - itemName - )); + setLayout(new BorderLayout()); + setBackground(ColorScheme.DARK_GRAY_COLOR); + setBorder(new EmptyBorder(7, 0, 0, 0)); - // The default border color is kind of dark, so we change it to something lighter - itemName.setBorder(BorderFactory.createLineBorder(getBackground().brighter())); + final MouseListener ml = new MouseAdapter() + { + @Override + public void mousePressed(MouseEvent mouseEvent) + { + super.mousePressed(mouseEvent); + switchPanel(); + } - progressBar.setStringPainted(true); + @Override + public void mouseEntered(MouseEvent mouseEvent) + { + super.mouseEntered(mouseEvent); + container.setBackground(ColorScheme.MEDIUM_GRAY_COLOR.brighter()); + } - setLayout(cardLayout); + @Override + public void mouseExited(MouseEvent mouseEvent) + { + super.mouseExited(mouseEvent); + container.setBackground(ColorScheme.MEDIUM_GRAY_COLOR); + } + }; - // 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)); + container.setLayout(cardLayout); + container.setBackground(ColorScheme.MEDIUM_GRAY_COLOR); - infoCard.setLayout(new BoxLayout(infoCard, BoxLayout.X_AXIS)); - // Icon on the left - infoCard.add(itemIcon); + JPanel faceCard = new JPanel(); + faceCard.setOpaque(false); + faceCard.setLayout(new BorderLayout()); + faceCard.addMouseListener(ml); - // 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); + itemIcon.setVerticalAlignment(JLabel.CENTER); + itemIcon.setHorizontalAlignment(JLabel.CENTER); + itemIcon.setPreferredSize(new Dimension(45, 45)); - // 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()); + itemName.setForeground(Color.WHITE); + itemName.setVerticalAlignment(JLabel.BOTTOM); + itemName.setFont(FontManager.getRunescapeSmallFont()); - cardLayout.show(this, EMPTY_CARD); + offerInfo.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + offerInfo.setVerticalAlignment(JLabel.TOP); + offerInfo.setFont(FontManager.getRunescapeSmallFont()); + + switchFaceViewIcon.setIcon(RIGHT_ARROW_ICON); + switchFaceViewIcon.setVerticalAlignment(JLabel.CENTER); + switchFaceViewIcon.setHorizontalAlignment(JLabel.CENTER); + switchFaceViewIcon.setPreferredSize(new Dimension(30, 45)); + + JPanel offerFaceDetails = new JPanel(); + offerFaceDetails.setOpaque(false); + offerFaceDetails.setLayout(new GridLayout(2, 1, 0, 2)); + + offerFaceDetails.add(itemName); + offerFaceDetails.add(offerInfo); + + faceCard.add(offerFaceDetails, BorderLayout.CENTER); + faceCard.add(itemIcon, BorderLayout.WEST); + faceCard.add(switchFaceViewIcon, BorderLayout.EAST); + + JPanel detailsCard = new JPanel(); + detailsCard.setOpaque(false); + detailsCard.setLayout(new BorderLayout()); + detailsCard.setBorder(new EmptyBorder(0, 15, 0, 0)); + detailsCard.addMouseListener(ml); + + itemPrice.setForeground(Color.WHITE); + itemPrice.setVerticalAlignment(JLabel.BOTTOM); + itemPrice.setFont(FontManager.getRunescapeSmallFont()); + + offerSpent.setForeground(Color.WHITE); + offerSpent.setVerticalAlignment(JLabel.TOP); + offerSpent.setFont(FontManager.getRunescapeSmallFont()); + + switchDetailsViewIcon.setIcon(LEFT_ARROW_ICON); + switchDetailsViewIcon.setVerticalAlignment(JLabel.CENTER); + switchDetailsViewIcon.setHorizontalAlignment(JLabel.CENTER); + switchDetailsViewIcon.setPreferredSize(new Dimension(30, 45)); + + JPanel offerDetails = new JPanel(); + offerDetails.setOpaque(false); + offerDetails.setLayout(new GridLayout(2, 1)); + + offerDetails.add(itemPrice); + offerDetails.add(offerSpent); + + detailsCard.add(offerDetails, BorderLayout.CENTER); + detailsCard.add(switchDetailsViewIcon, BorderLayout.EAST); + + container.add(faceCard, FACE_CARD); + container.add(detailsCard, DETAILS_CARD); + + cardLayout.show(container, FACE_CARD); + + add(container, BorderLayout.CENTER); + add(progressBar, BorderLayout.SOUTH); } - + void updateOffer(ItemComposition offerItem, BufferedImage itemImage, @Nullable GrandExchangeOffer newOffer) { if (newOffer == null || newOffer.getState() == EMPTY) { - cardLayout.show(this, EMPTY_CARD); - itemName.setTitle("Nothing"); + return; } else { - cardLayout.show(this, INFO_CARD); + cardLayout.show(container, FACE_CARD); - itemName.setTitle(offerItem.getName()); + itemName.setText(offerItem.getName()); + itemIcon.setIcon(new ImageIcon(itemImage)); - boolean shouldStack = offerItem.isStackable() || newOffer.getTotalQuantity() > 1; - ImageIcon newItemIcon = new ImageIcon(itemImage); - itemIcon.setIcon(newItemIcon); + boolean buying = newOffer.getState() == GrandExchangeOfferState.BOUGHT + || newOffer.getState() == GrandExchangeOfferState.BUYING + || newOffer.getState() == GrandExchangeOfferState.CANCELLED_BUY; - offerState.setText(getNameForState(newOffer.getState()) - + " at " - + StackFormatter.formatNumber(newOffer.getState() == GrandExchangeOfferState.BOUGHT ? (newOffer.getSpent() / newOffer.getTotalQuantity()) : newOffer.getPrice()) - + (newOffer.getTotalQuantity() > 1 ? " gp ea" : " gp")); + String offerState = (buying ? "Bought " : "Sold ") + + StackFormatter.quantityToRSDecimalStack(newOffer.getQuantitySold()) + " / " + + StackFormatter.quantityToRSDecimalStack(newOffer.getTotalQuantity()); - progressBar.setMaximum(newOffer.getTotalQuantity()); + offerInfo.setText(offerState); + + itemPrice.setText(htmlLabel("Price each: ", newOffer.getPrice() + "")); + + String action = buying ? "Spent: " : "Received: "; + + offerSpent.setText(htmlLabel(action, StackFormatter.formatNumber(newOffer.getSpent()) + " / " + + StackFormatter.formatNumber(newOffer.getPrice() * newOffer.getTotalQuantity()))); + + progressBar.setForeground(getProgressColor(newOffer)); + progressBar.setMaximumValue(newOffer.getTotalQuantity()); progressBar.setValue(newOffer.getQuantitySold()); - progressBar.setBackground(getColorForState(newOffer.getState())); - progressBar.setString(newOffer.getQuantitySold() + "/" + newOffer.getTotalQuantity()); + progressBar.update(); + + /* Couldn't set the tooltip for the container panel as the children override it, so I'm setting + * the tooltips on the children instead. */ + for (Component c : container.getComponents()) + { + if (c instanceof JPanel) + { + JPanel panel = (JPanel) c; + panel.setToolTipText(htmlTooltip(((int) progressBar.getPercentage()) + "%")); + } + } } + + revalidate(); repaint(); } - private String getNameForState(GrandExchangeOfferState state) + private String htmlTooltip(String value) { - switch (state) - { - case CANCELLED_BUY: - return "Buying cancelled"; - case CANCELLED_SELL: - return "Selling cancelled"; - case BUYING: - return "Buying"; - case BOUGHT: - return "Bought"; - case SELLING: - return "Selling"; - case SOLD: - return "Sold"; - case EMPTY: - default: - return "Empty"; - - } + return "Progress: " + value + ""; } - private Color getColorForState(GrandExchangeOfferState state) + private String htmlLabel(String key, String value) { - switch (state) - { - case CANCELLED_BUY: - case CANCELLED_SELL: - 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; - } + return "" + key + "" + value + ""; } + private void switchPanel() + { + this.showingFace = !this.showingFace; + cardLayout.show(container, showingFace ? FACE_CARD : DETAILS_CARD); + } + + private Color getProgressColor(GrandExchangeOffer offer) + { + if (offer.getState() == CANCELLED_BUY || offer.getState() == CANCELLED_SELL) + { + return ColorScheme.PROGRESS_ERROR_COLOR; + } + + if (offer.getQuantitySold() == offer.getTotalQuantity()) + { + return ColorScheme.PROGRESS_COMPLETE_COLOR; + } + + return ColorScheme.PROGRESS_INPROGRESS_COLOR; + } } - diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeOffersPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeOffersPanel.java new file mode 100644 index 0000000000..fb13394a05 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeOffersPanel.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2018, SomeoneWithAnInternetConnection + * Copyright (c) 2018, Psikoi + * 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.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.image.BufferedImage; +import java.util.concurrent.ScheduledExecutorService; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; +import net.runelite.api.Client; +import net.runelite.api.GrandExchangeOffer; +import net.runelite.api.GrandExchangeOfferState; +import net.runelite.api.ItemComposition; +import net.runelite.client.game.ItemManager; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.components.PluginErrorPanel; + +public class GrandExchangeOffersPanel extends JPanel +{ + private static final String ERROR_PANEL = "ERROR_PANEL"; + private static final String OFFERS_PANEL = "OFFERS_PANEL"; + + private static final int MAX_OFFERS = 8; + + private final GridBagConstraints constraints = new GridBagConstraints(); + private final CardLayout cardLayout = new CardLayout(); + + /* The offers container, this will hold all the individual ge offers panels */ + private final JPanel offerPanel = new JPanel(); + + /* The error panel, this displays an error message */ + private final PluginErrorPanel errorPanel = new PluginErrorPanel(); + + /* The center panel, this holds either the error panel or the offers container */ + private final JPanel container = new JPanel(cardLayout); + + private final Client client; + private final ItemManager itemManager; + private final ScheduledExecutorService executor; + + private GrandExchangeOfferSlot[] offerSlotPanels = new GrandExchangeOfferSlot[MAX_OFFERS]; + + public GrandExchangeOffersPanel(Client client, ItemManager itemManager, ScheduledExecutorService executor) + { + this.client = client; + this.itemManager = itemManager; + this.executor = executor; + init(); + } + + void init() + { + setLayout(new BorderLayout()); + setBackground(ColorScheme.DARK_GRAY_COLOR); + + constraints.fill = GridBagConstraints.HORIZONTAL; + constraints.weightx = 1; + constraints.gridx = 0; + constraints.gridy = 0; + + /* This panel wraps the offers panel and limits its height */ + JPanel offersWrapper = new JPanel(new BorderLayout()); + offersWrapper.setBackground(ColorScheme.DARK_GRAY_COLOR); + offersWrapper.add(offerPanel, BorderLayout.NORTH); + + offerPanel.setLayout(new GridBagLayout()); + offerPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); + offerPanel.setBackground(ColorScheme.DARK_GRAY_COLOR); + + /* This panel wraps the error panel and limits its height */ + JPanel errorWrapper = new JPanel(new BorderLayout()); + errorWrapper.setBackground(ColorScheme.DARK_GRAY_COLOR); + errorWrapper.add(errorPanel, BorderLayout.NORTH); + + errorPanel.setBorder(new EmptyBorder(50, 20, 20, 20)); + errorPanel.setContent("No offers detected", "No grand exchange offers were found on your account."); + + container.add(offersWrapper, OFFERS_PANEL); + container.add(errorWrapper, ERROR_PANEL); + + add(container, BorderLayout.CENTER); + + resetOffers(); + } + + void resetOffers() + { + offerPanel.removeAll(); + for (int i = 0; i < offerSlotPanels.length; i++) + { + offerSlotPanels[i] = null; + } + updateEmptyOffersPanel(); + } + + void updateOffer(ItemComposition item, BufferedImage itemImage, GrandExchangeOffer newOffer, int slot) + { + /* If slot was previously filled, and is now empty, remove it from the list */ + if (newOffer == null || newOffer.getState() == GrandExchangeOfferState.EMPTY) + { + if (offerSlotPanels[slot] != null) + { + offerPanel.remove(offerSlotPanels[slot]); + offerSlotPanels[slot] = null; + revalidate(); + repaint(); + } + + removeTopMargin(); + updateEmptyOffersPanel(); + return; + } + + /* If slot was empty, and is now filled, add it to the list */ + if (offerSlotPanels[slot] == null) + { + GrandExchangeOfferSlot newSlot = new GrandExchangeOfferSlot(); + offerSlotPanels[slot] = newSlot; + offerPanel.add(newSlot, constraints); + constraints.gridy++; + } + + offerSlotPanels[slot].updateOffer(item, itemImage, newOffer); + + removeTopMargin(); + + revalidate(); + repaint(); + + updateEmptyOffersPanel(); + } + + /** + * Reset the border for the first offer slot. + */ + private void removeTopMargin() + { + + if (offerPanel.getComponentCount() <= 0) + { + return; + } + + JPanel firstItem = (JPanel) offerPanel.getComponent(0); + firstItem.setBorder(null); + } + + /** + * This method calculates the amount of empty ge offer slots, if all slots are empty, + * it shows the error panel. + */ + private void updateEmptyOffersPanel() + { + int nullCount = 0; + for (GrandExchangeOfferSlot slot : offerSlotPanels) + { + if (slot == null) + { + nullCount++; + } + } + + if (nullCount == MAX_OFFERS) + { + offerPanel.removeAll(); + cardLayout.show(container, ERROR_PANEL); + } + else + { + cardLayout.show(container, OFFERS_PANEL); + } + + } + +} 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 3f84da8e01..b8bf685117 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2018, SomeoneWithAnInternetConnection + * Copyright (c) 2018, Psikoi * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -26,58 +27,58 @@ package net.runelite.client.plugins.grandexchange; import java.awt.BorderLayout; -import java.awt.image.BufferedImage; import java.util.concurrent.ScheduledExecutorService; import javax.inject.Inject; -import javax.swing.BoxLayout; import javax.swing.JPanel; -import javax.swing.JTabbedPane; +import javax.swing.border.EmptyBorder; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import net.runelite.api.Client; -import net.runelite.api.GrandExchangeOffer; -import net.runelite.api.ItemComposition; import net.runelite.client.game.ItemManager; +import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.PluginPanel; +import net.runelite.client.ui.components.materialtabs.MaterialTab; +import net.runelite.client.ui.components.materialtabs.MaterialTabGroup; @Slf4j class GrandExchangePanel extends PluginPanel { - private static final int MAX_OFFERS = 8; + + // this panel will hold either the ge search panel or the ge offers panel + private final JPanel display = new JPanel(); + + private final MaterialTabGroup tabGroup = new MaterialTabGroup(display); + private final MaterialTab searchTab; @Getter private GrandExchangeSearchPanel searchPanel; - - private GrandExchangeOfferSlot[] offerSlotPanels = new GrandExchangeOfferSlot[MAX_OFFERS]; - - private JPanel offerPanel = new JPanel(); - - private JTabbedPane tabbedPane = new JTabbedPane(); + @Getter + private GrandExchangeOffersPanel offersPanel; @Inject GrandExchangePanel(Client client, ItemManager itemManager, ScheduledExecutorService executor) { - setLayout(new BorderLayout()); - add(tabbedPane, BorderLayout.NORTH); + super(false); - // Offer Panel - offerPanel.setLayout(new BoxLayout(offerPanel, BoxLayout.Y_AXIS)); - for (int i = 0; i < offerSlotPanels.length; ++i) - { - offerSlotPanels[i] = new GrandExchangeOfferSlot(); - offerPanel.add(offerSlotPanels[i]); - } + setLayout(new BorderLayout()); + setBackground(ColorScheme.DARK_GRAY_COLOR); // Search Panel searchPanel = new GrandExchangeSearchPanel(client, itemManager, executor); - tabbedPane.addTab("Offers", offerPanel); - tabbedPane.addTab("Search", searchPanel); - } + //Offers Panel + offersPanel = new GrandExchangeOffersPanel(client, itemManager, executor); - void updateOffer(ItemComposition item, BufferedImage itemImage, GrandExchangeOffer newOffer, int slot) - { - offerSlotPanels[slot].updateOffer(item, itemImage, newOffer); + MaterialTab offersTab = new MaterialTab("Offers", tabGroup, offersPanel); + searchTab = new MaterialTab("Search", tabGroup, searchPanel); + + tabGroup.setBorder(new EmptyBorder(5, 0, 0, 0)); + tabGroup.addTab(offersTab); + tabGroup.addTab(searchTab); + tabGroup.select(offersTab); // selects the default selected tab + + add(tabGroup, BorderLayout.NORTH); + add(display, BorderLayout.CENTER); } void showSearch() @@ -87,7 +88,7 @@ class GrandExchangePanel extends PluginPanel return; } - tabbedPane.setSelectedComponent(searchPanel); + tabGroup.select(searchTab); revalidate(); } -} +} \ No newline at end of file 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 1c10fbcd7d..21078d646f 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 @@ -44,6 +44,7 @@ import net.runelite.api.ItemComposition; import net.runelite.api.MenuEntry; import net.runelite.api.events.ConfigChanged; import net.runelite.api.events.FocusChanged; +import net.runelite.api.events.GameStateChanged; import net.runelite.api.events.GrandExchangeOfferChanged; import net.runelite.api.events.MenuEntryAdded; import net.runelite.api.widgets.WidgetID; @@ -161,7 +162,16 @@ public class GrandExchangePlugin extends Plugin ItemComposition offerItem = itemManager.getItemComposition(offer.getItemId()); boolean shouldStack = offerItem.isStackable() || offer.getTotalQuantity() > 1; BufferedImage itemImage = itemManager.getImage(offer.getItemId(), offer.getTotalQuantity(), shouldStack); - SwingUtilities.invokeLater(() -> panel.updateOffer(offerItem, itemImage, offerEvent.getOffer(), offerEvent.getSlot())); + SwingUtilities.invokeLater(() -> panel.getOffersPanel().updateOffer(offerItem, itemImage, offerEvent.getOffer(), offerEvent.getSlot())); + } + + @Subscribe + public void onGameStateChanged(GameStateChanged gameStateChanged) + { + if (gameStateChanged.getGameState() == GameState.LOGIN_SCREEN) + { + panel.getOffersPanel().resetOffers(); + } } @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 index 6372ea82c6..628658c179 100644 --- 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 @@ -1,5 +1,6 @@ /* * Copyright (c) 2018, Seth + * Copyright (c) 2018, Psikoi * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -26,9 +27,10 @@ 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.awt.CardLayout; +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -36,8 +38,8 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledExecutorService; import javax.imageio.ImageIO; import javax.swing.ImageIcon; -import javax.swing.JLabel; import javax.swing.JPanel; +import javax.swing.JScrollPane; import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; import lombok.extern.slf4j.Slf4j; @@ -45,26 +47,69 @@ import net.runelite.api.Client; import net.runelite.api.ItemComposition; import net.runelite.client.game.AsyncBufferedImage; import net.runelite.client.game.ItemManager; +import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.components.IconTextField; +import net.runelite.client.ui.components.PluginErrorPanel; import net.runelite.http.api.item.Item; import net.runelite.http.api.item.ItemPrice; import net.runelite.http.api.item.SearchResult; +/** + * This panel holds the search section of the Grand Exchange Plugin. + * It should display a search bar and either item results or a error panel. + */ @Slf4j class GrandExchangeSearchPanel extends JPanel { - private static final List ITEMS_LIST = new ArrayList<>(); + private static final String ERROR_PANEL = "ERROR_PANEL"; + private static final String RESULTS_PANEL = "RESULTS_PANEL"; + + private static final ImageIcon SEARCH_ICON; + private static final ImageIcon LOADING_ICON; + private static final ImageIcon ERROR_ICON; + + private final GridBagConstraints constraints = new GridBagConstraints(); + private final CardLayout cardLayout = new CardLayout(); private final Client client; private final ItemManager itemManager; private final ScheduledExecutorService executor; - private ImageIcon search; + private final IconTextField searchBox = new IconTextField(); - private IconTextField searchBox = new IconTextField(); - private JPanel container = new JPanel(); - private JPanel searchItemsPanel = new JPanel(); - private JLabel searchingLabel = new JLabel(); + /* The main container, this holds the search bar and the center panel */ + private final JPanel container = new JPanel(); + + /* The results container, this will hold all the individual ge item panels */ + private final JPanel searchItemsPanel = new JPanel(); + + /* The center panel, this holds either the error panel or the results container */ + private final JPanel centerPanel = new JPanel(cardLayout); + + /* The error panel, this displays an error message */ + private final PluginErrorPanel errorPanel = new PluginErrorPanel(); + + /* The results wrapper, this scrolling panel wraps the results container */ + private JScrollPane resultsWrapper; + + private List itemsList = new ArrayList<>(); + + static + { + try + { + synchronized (ImageIO.class) + { + SEARCH_ICON = new ImageIcon(ImageIO.read(IconTextField.class.getResourceAsStream("search_darker.png"))); + LOADING_ICON = new ImageIcon(IconTextField.class.getResource("loading_spinner.gif")); + ERROR_ICON = new ImageIcon(ImageIO.read(IconTextField.class.getResourceAsStream("error.png"))); + } + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } GrandExchangeSearchPanel(Client client, ItemManager itemManager, ScheduledExecutorService executor) { @@ -77,38 +122,54 @@ class GrandExchangeSearchPanel extends JPanel void init() { setLayout(new BorderLayout()); - container.setLayout(new BorderLayout()); + setBackground(ColorScheme.DARK_GRAY_COLOR); - // Search Box - try - { - BufferedImage icon; - synchronized (ImageIO.class) - { - icon = ImageIO.read(GrandExchangePlugin.class.getResourceAsStream("search.png")); - } - search = new ImageIcon(icon); - } - catch (IOException e) - { - log.warn("Failed to read icon", e); - } + container.setLayout(new BorderLayout(5, 5)); + container.setBorder(new EmptyBorder(10, 10, 10, 10)); + container.setBackground(ColorScheme.DARK_GRAY_COLOR); - searchBox.setIcon(search); + searchBox.setPreferredSize(new Dimension(100, 30)); + searchBox.setBackground(ColorScheme.MEDIUM_GRAY_COLOR); + searchBox.setHoverBackgroundColor(ColorScheme.MEDIUM_GRAY_COLOR.brighter()); + searchBox.setIcon(SEARCH_ICON); searchBox.addActionListener(e -> executor.execute(() -> priceLookup(false))); + searchItemsPanel.setLayout(new GridBagLayout()); + searchItemsPanel.setBackground(ColorScheme.DARK_GRAY_COLOR); + + constraints.fill = GridBagConstraints.HORIZONTAL; + constraints.weightx = 1; + constraints.gridx = 0; + constraints.gridy = 0; + + /* This panel wraps the results panel and guarantees the scrolling behaviour */ + JPanel wrapper = new JPanel(new BorderLayout()); + wrapper.setBackground(ColorScheme.DARK_GRAY_COLOR); + wrapper.add(searchItemsPanel, BorderLayout.NORTH); + + resultsWrapper = new JScrollPane(wrapper); + resultsWrapper.setBackground(ColorScheme.DARK_GRAY_COLOR); + resultsWrapper.getVerticalScrollBar().setPreferredSize(new Dimension(12, 0)); + resultsWrapper.getVerticalScrollBar().setBorder(new EmptyBorder(0, 5, 0, 0)); + resultsWrapper.setVisible(false); + + /* This panel wraps the error panel and limits its height */ + JPanel errorWrapper = new JPanel(new BorderLayout()); + errorWrapper.setBackground(ColorScheme.DARK_GRAY_COLOR); + errorWrapper.add(errorPanel, BorderLayout.NORTH); + + errorPanel.setContent("Grand Exchange Search", + "Here you can search for an item by its name to find price information."); + + centerPanel.add(resultsWrapper, RESULTS_PANEL); + centerPanel.add(errorWrapper, ERROR_PANEL); + + cardLayout.show(centerPanel, ERROR_PANEL); + container.add(searchBox, BorderLayout.NORTH); + container.add(centerPanel, BorderLayout.CENTER); - // 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); + add(container, BorderLayout.CENTER); } void priceLookup(String item) @@ -129,7 +190,9 @@ class GrandExchangeSearchPanel extends JPanel // Input is not empty, add searching label searchItemsPanel.removeAll(); - showSearchString("Searching..."); + searchBox.setBackground(ColorScheme.MEDIUM_GRAY_COLOR); + searchBox.setEditable(false); + searchBox.setIcon(LOADING_ICON); SearchResult result; @@ -140,13 +203,19 @@ class GrandExchangeSearchPanel extends JPanel catch (ExecutionException ex) { log.warn("Unable to search for item {}", lookup, ex); - showSearchString("Error performing search"); + searchBox.setIcon(ERROR_ICON); + searchBox.setEditable(true); + errorPanel.setContent("Error fetching results", "An error occured why trying to fetch item data, please try again later."); + cardLayout.show(centerPanel, ERROR_PANEL); return; } + itemsList.clear(); if (result != null && !result.getItems().isEmpty()) { + cardLayout.show(centerPanel, RESULTS_PANEL); + for (Item item : result.getItems()) { int itemId = item.getId(); @@ -169,7 +238,7 @@ class GrandExchangeSearchPanel extends JPanel AsyncBufferedImage itemImage = itemManager.getImage(itemId); - ITEMS_LIST.add(new GrandExchangeItems(itemImage, item.getName(), itemId, itemPrice != null ? itemPrice.getPrice() : 0, itemComp.getPrice() * 0.6)); + itemsList.add(new GrandExchangeItems(itemImage, item.getName(), itemId, itemPrice != null ? itemPrice.getPrice() : 0, itemComp.getPrice() * 0.6)); // If using hotkey to lookup item, stop after finding match. if (exactMatch && item.getName().equalsIgnoreCase(lookup)) @@ -178,44 +247,51 @@ class GrandExchangeSearchPanel extends JPanel } } } + else + { + searchBox.setIcon(ERROR_ICON); + errorPanel.setContent("No results found.", "No items were found with that name, please try again."); + cardLayout.show(centerPanel, ERROR_PANEL); + } SwingUtilities.invokeLater(() -> { - if (ITEMS_LIST.isEmpty()) + int index = 0; + for (GrandExchangeItems item : itemsList) { - showSearchString("No results found."); - } - else - { - for (GrandExchangeItems item : ITEMS_LIST) + GrandExchangeItemPanel panel = new GrandExchangeItemPanel(item.getIcon(), item.getName(), + item.getItemId(), item.getGePrice(), item.getHaPrice()); + + /* + Add the first item directly, wrap the rest with margin. This margin hack is because + gridbaglayout does not support inter-element margins. + */ + if (index++ > 0) { - GrandExchangeItemPanel panel = new GrandExchangeItemPanel(item.getIcon(), item.getName(), - item.getItemId(), item.getGePrice(), item.getHaPrice()); - - searchItemsPanel.add(panel); + JPanel marginWrapper = new JPanel(new BorderLayout()); + marginWrapper.setBackground(ColorScheme.DARK_GRAY_COLOR); + marginWrapper.setBorder(new EmptyBorder(5, 0, 0, 0)); + marginWrapper.add(panel, BorderLayout.NORTH); + searchItemsPanel.add(marginWrapper, constraints); } + else + { + searchItemsPanel.add(panel, constraints); + } + + constraints.gridy++; + } - // Remove searching label after search is complete - showSearchString(null); - ITEMS_LIST.clear(); + // remove focus from the search bar + searchItemsPanel.requestFocusInWindow(); + searchBox.setEditable(true); + + // Remove searching label after search is complete + if (!itemsList.isEmpty()) + { + searchBox.setIcon(SEARCH_ICON); } }); } - private void showSearchString(String str) - { - if (str != null) - { - remove(searchingLabel); - searchingLabel.setText(str); - add(searchingLabel, BorderLayout.CENTER); - } - else - { - remove(searchingLabel); - } - - revalidate(); - repaint(); - } -} +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/ui/components/PluginErrorPanel.java b/runelite-client/src/main/java/net/runelite/client/ui/components/PluginErrorPanel.java index 779253cff0..e7b2fad971 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/components/PluginErrorPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/components/PluginErrorPanel.java @@ -46,7 +46,7 @@ public class PluginErrorPanel extends JPanel public PluginErrorPanel() { setOpaque(false); - setBorder(new EmptyBorder(50, 0, 0, 0)); + setBorder(new EmptyBorder(50, 10, 0, 10)); setLayout(new BorderLayout()); noResultsTitle.setForeground(Color.WHITE); diff --git a/runelite-client/src/main/java/net/runelite/client/util/StackFormatter.java b/runelite-client/src/main/java/net/runelite/client/util/StackFormatter.java index 607da65d91..777688f84a 100644 --- a/runelite-client/src/main/java/net/runelite/client/util/StackFormatter.java +++ b/runelite-client/src/main/java/net/runelite/client/util/StackFormatter.java @@ -128,6 +128,61 @@ public class StackFormatter } } + /** + * Convert a quantity to stack size as it would + * appear in RuneScape. (with decimals) + *

+ * This differs from quantityToRSStack in that it displays + * decimals. Ex: 27100 is 27,1k (not 27k) + *

+ * This uses the NumberFormat singleton instead of the + * NUMBER_FORMATTER variable to ensure the UK locale. + * + * @param quantity The quantity to convert. + * @return The stack size as it would appear in RS, with decimals, + * with K after 100,000 and M after 10,000,000 + */ + public static String quantityToRSDecimalStack(int quantity) + { + + if (quantity < 10_000) + { + return Integer.toString(quantity); + } + else if (quantity < 1_000_000) + { + if (quantity % 1000 == 0) + { + return quantity / 1000 + "K"; + } + return NUMBER_FORMATTER.format(quantity).substring(0, Integer.toString(quantity).length() - 1) + "K"; + } + else if (quantity < 10_000_000) + { + if (quantity % 1_000_000 == 0) + { + return quantity / 1_000_000 + "M"; + } + return NUMBER_FORMATTER.format(quantity).substring(0, Integer.toString(quantity).length() - 4) + "M"; + } + else if (quantity < 1_000_000_000) + { + if (quantity % 1_000_000 == 0) + { + return quantity / 1_000_000 + "M"; + } + return NUMBER_FORMATTER.format(quantity).substring(0, Integer.toString(quantity).length() - 4) + "M"; + } + else + { + if (quantity % 1_000_000_000 == 0) + { + return quantity / 1_000_000_000 + "B"; + } + return NUMBER_FORMATTER.format(quantity).substring(0, Integer.toString(quantity).length() - 7) + "B"; + } + } + /** * Converts a string representation of a stack * back to (close to) it's original value. @@ -147,8 +202,8 @@ public class StackFormatter * * @param number the long number to format * @return the formatted String - * @exception ArithmeticException if rounding is needed with rounding - * mode being set to RoundingMode.UNNECESSARY + * @throws ArithmeticException if rounding is needed with rounding + * mode being set to RoundingMode.UNNECESSARY * @see java.text.Format#format */ public static String formatNumber(final long number) @@ -161,8 +216,8 @@ public class StackFormatter * * @param number the double number to format * @return the formatted String - * @exception ArithmeticException if rounding is needed with rounding - * mode being set to RoundingMode.UNNECESSARY + * @throws ArithmeticException if rounding is needed with rounding + * mode being set to RoundingMode.UNNECESSARY * @see java.text.Format#format */ public static String formatNumber(double number) diff --git a/runelite-client/src/main/java/net/runelite/client/util/SwingUtil.java b/runelite-client/src/main/java/net/runelite/client/util/SwingUtil.java index a83cd7e24e..674ebe07dd 100644 --- a/runelite-client/src/main/java/net/runelite/client/util/SwingUtil.java +++ b/runelite-client/src/main/java/net/runelite/client/util/SwingUtil.java @@ -94,6 +94,14 @@ public class SwingUtil System.setProperty("sun.awt.noerasebackground", "true"); } + /** + * Converts a given color to it's hexidecimal equivalent. + */ + public static String toHexColor(Color color) + { + return "#" + Integer.toHexString(color.getRGB()).substring(2); + } + /** * Safely sets Swing theme * diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/grandexchange/arrow_left.png b/runelite-client/src/main/resources/net/runelite/client/plugins/grandexchange/arrow_left.png new file mode 100644 index 0000000000000000000000000000000000000000..480358e6a1c6da1f527110a7605402f6b162c0ca GIT binary patch literal 15918 zcmeI3e^?Xe9l*~g(kbdzYO63?jZvqRCU=(*Le3Dx1SMNz`4KBns$A|aa2j$k2@ud> zk+#;^R%@-)U#Qz~6RWOWwR38rDz3UJ57kFIZHHL!*TmXci>=P=y@dSW1@7^A+CBT{ z?s<~C@B96A_j%v%_q~5^dFHH{BcgvEjUdPfQ-(1coFCzTLk5HYdw-kpIXDegtcCOxlwST-%39B!f9)MR9r`L74@*JT;2RA3y~ zjC{s4KONXeYTQ+6OqQ*3#`2|G>+9>6QopRqZkX<57MkWe-d$QW*Hx93R+VKn&5s7g zr+@koIOf*gdnBs+$S1legJkwYPu9Hh@sy}vT8`}qqPUdt7j5vGt8HEUVnC9Wd$0A zzcl$b8`i8}W3ZgIR5jG)JonqVJ7eD2{=4RFw=))PK575r$%9jVogj&;S@QDAS6?5q z8((M7XV%(pP^4o!-CT70{P*=IMw}m6HnnTc)K5w`UO9cOpfmfz`WvGzkG&Cd;c92+ zwXQ4FhZyU-;+>~FtTBOL(ysb z&V!cjt<{ZLyN~VGHZs%ywQX-gSKHM3{nz)uxrkVF_V)8#K2;X8x}{@v{H<+uZ-2|a zeEsQ`y!tMaTFxCUJ%6V0AAfv5 z-gWDUeDYr0@xi&Q1MaR}qZh6D_Gs6JJ-%~k?S|@QcUNvqIqLrWuCe==>JJqs?`+!S zyzyyN>x!7#O9fTV*`4`kZr|2?_|NzDeX;oR>6EQ^TYt7zxA^X7_e@`&QnfVCn|QFG z`&##yl^^7^rQ9#P^KSd8@6dnGyVcm`8-G)E_3YYI`;rOwci#7nLk1s`o*44`HLVCj zpsXd=lWTsOptuqRNpn_4;Vp53ib4=ws@F+U3mA{o$`rAV6xmn%8)Z_KPLa(~n=!M~ zz!b9?l`bZya+ZavTtI1QS*ku-=OsXZ62?PHy(M;soA9Q{{Ba3z%@3n8so%u2AVrqO zCzR%zGo=R3#YokPBsqm8X`~vh0#|D^iJGURN(@({m}73C@g=PE*RtyYU-N>r(o0}Hvk!r>vka)*0DAV@Hd zk#SQl*6Cq6hm?;?TDekBicH2Q3LQPqRpJaKa=3fg0g9-XbfUNdL;G{0sgRGe)MfYI z8cm^$ohe})9yjpA`}K1MyI9D*SIUrjz{Frq%;x@`gZEof(r;?FC%p_H=t0^mZP3$g zscT@;gE1|DTXe^2jWZQ;r36^Yjjc<+pt9l(v(1l;^fAuvNa@T`Qv#gHD( zW#KrxKCsHd8v)n{_*EsHGKY20T)8`5hr%x5`3^^8jHHLr1J7hRmMF*77ED8^wS-EY zfZ+s&MFa_0LNUx7&DtvZ#7HJEHKEo-#)zIlxN!lZ9_-CO(GL6zO9gFj~m-}Iv)%{Aby1Vz&Xl`8HZ4X215C*1`gupY@Tw`#iSL174jg> zBM`N>8ysGT^4p1>{U1z(n=Jcb)9_$b^+TosYI6U-X$Y@ks+e>XF|-~HF5uw6gUwuE zK6_VIuv+_8qK@C+2vEgfMUnpM0e!U3SU7XQK9{dXd#T@Eqr||0;{1P)Yor=>%@cNw zRKu=e&)E*vqX#|6f1}WFgcrXF2X=@Y@V?0ClhkUJT8F~Jutz5AW$d{|7W82^-%s%5 z9v@iIuVdaojuDC;$G}uYyxI9{Ffa<<8^J3p8hU5#Q=XwN_G4azpXkSo3dItWKtO~= z5tlF@NGsxkfC!5sE@3{9R>TDX5f(*U!h9gDhzkNDEQ+{<`9N9`7X(CD6mbdjfwUqn z2#ByK;u7WqX+>NR5MfcoCCmrXint&k!lH;vm=B~CaX~P5f=nRSQK#y^MSM?E(nOQDB=?418GHE5D;Ne z#3jrJ(u%ksAi|=EOPCL&6>&j8ghdgTFds-O;(~w(iy|&zK9E+#1pyHjMO?ysAgzcC z0wOF5;))J`1Xs-`J#p{OFqA4Md9!vvB*Tp X%)ui*+c=T`TGf<3%eZTL;nM#Ctaa;x literal 0 HcmV?d00001 diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/grandexchange/arrow_right.png b/runelite-client/src/main/resources/net/runelite/client/plugins/grandexchange/arrow_right.png new file mode 100644 index 0000000000000000000000000000000000000000..63ab23f273cfcdbd6ff203ecf37900db0eb23d00 GIT binary patch literal 15917 zcmeI3dvw#*8NkJH(m^QQ3@yZC1!G_(q_!kK;#fEi!4AZ24S5(yXn4r7oCs{o*iPaE z76>V&^Z|@g(((wUBWyr{wF5dz8G#a37?4pout7T>O?Zs}9fi=r@UHB{c5V_K&S`h{ zk93YLefN7wzk9#$>K`r69zU*6!p{>B1nFbWGUbAEU+&j44*Wm-^VqMzsh20Kz=t5c z`*FV>$f`Ao2qH>kjK=KjV%E?4idm1;Y&1$eUe>`lX#@$JrDn{iy>RtX(~)8I_1W3h z+@?WIi4w7-uD-Ii>aD8xs<>kazWa~$xU_x|ca5f_@AMGO-E*k^i~9O{Pr;cJ4Ts;U zQrE94%(fl?1M{n@_Kz68GZnaA>@5-*E6U9oy+k7FNs*{JE4MUs-GjR>&03}cIra6t9jTrzV+ZyeY{aL@rjYuuYEBh{=3^5uMHXPWpX61G;lZh{;_4B|GxUyOYt|p zeg5sFlQxI=vZ8ydj${3=-_sh;=h(l?h3+>XEJ zoI#g~3EJpD-AUv?%JCimMsb92!t=hnT} z*7a5Ua<(7duG>eC`RB%6$t~wc)$O^l=j}PfoHKW)wggo<^zz20dG*Fi zjRkdo7;s#d-r6{~B|fM3r~xg74`*)rPPwkRBspc<+>fW&-fBF1sN~$~eSiPmjwJ7$ zgYxHg;qFh)=6vRB**0L#ihGAzR__d+&A4c+TJ&J)nzTc{uO66M537D#e*FI0wVs<_ z#-DgYTywdw(lfDn%IUjzwSW4@$Gg9td*xKx`UfX|x>7&)!QUR5zd5ODY?wZ5Z(-~8 z)`3eu$vdC+XvY1IE}r}z{nzw6`&xoSZmX`HS()yfH}uigN5R2J+yUvao*P!2KoA0D ztoiZY_i6JSv*F% zn8~W}(s>o*tyIM0+-6`{rmwNl!4$P5YKO7FEh zh+Naywsb&BlNI~@9s)(n%E}aFDh2B;LUEl=hhj=psgwf?xv$*qCj)Y~Z)iA3dmIz( zqr8mA&#-PO7niiLCH^#-j7tkV=V|X^5%&%$BkFDwgE_HSx^`~AUze-P)INV^DL~MMv_sl}dl%ZWcy`OPJC#ORsPSfqZ(*dR%H%=3F3oV844C%qM5=Jjg`dP1) zWu1obDvNFeU?1RCm2|{p#%*WId`Wr~c8Sh+G#YIp{j>pirpht39M@PeEuqm7Dorwm z6Brf~By0)Au&{Q$j7dz0&|)Rb zZl9lYQ?%J+00Rn!u@iP&n`%=kZE{-Wu*q?oS|cY(wOX!G*(jTaN~P>7Dl`ukORRgR z&?c5D;R+6f4)-QIOM&pwCsR>Mt&L8_zAm2d1&>?3mI4V24q35vEhB zEXD^Ge0fYEfN}^m$2@v=$I*6$lL<9B59#&MTnVSi+G{w{6(dcL^9i*Af~2_iXrQxC@ZGjd=(lF6|(9+h>vyn%SbPsQ3O`V<1~+O z)Q)a&cs_owqzx$=jY^|O;bGV#n+ed)d=mruu#f8}cxsyu ztmx9QpgYGHMVn)Is$$;kLNyp31@DdEl@*P=vvw-aNEiDtFQQNMV@8Ey2}vLz{Gx!1 zpAV!Ja6v%$MFAH-A4n_Uf`IUg0xo_&kXFD20pS+~T>N|>t$+&x!Y>NA`1wFu0T%>> zUlefh^MSMiE(i#}DB$Af18D_Z5Dvd7_(cI1KOaad z;DUhgivliwK9E+x1p(m~1zh}mAgzE40>Uo}xcK=%S^*aXgkKbJ@$-SS0xk#$zbN41 z=L2a4To4d`QNYE|2hs|-ARzprfQz3Gq!n;MK=?%g7e60JE8v2F@QVU2em;;^zy$%} z7kP0dM85*1-Qd&DGVqP(1kKS8!S|t3Dl6B5AQi(AWPUY*d>;hos|b?Y6Y23xKKQh> z20;d}XQm8Z25hZnQ-(Eg)^@*fbfWUf4YG|vWl+*9sqdp(x2F%xlYFX>IFDdc=VwY% z79IId+5PPcr;G2*`#g9rnE7qefe+%QiYE=f`g&aO9656TjwPxnk$q literal 0 HcmV?d00001 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 deleted file mode 100644 index 23bf66b759f5dbd8eea47cd1316c5fdb0e2c9202..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 388 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf4nJ z$hLzpWB=2SsX#%=64!{5;QX|b^2DN4hVt@qz0ADq;^f4FRK5J7^x5xhq!<_&89iMb zLo9lyUfS!$94Ns0;Cu+i8 z{}%+OUeN7azj(Hh*$1IN!n1ZBTrg!$xB3m8)|{tLq6L1NCS5XZFMPMc$7F-h*WWzN zN=*xawL83^_38@w;!X0L%9#nF7M;e)Yo4Y=-zrx0-Mwd(Sf6vm1HE1IocwQn zPPycK=h5LWi+8Oue(6ztj`gv}ywgiG-~JR>exuvk+{t9Rr|c@9bK-^ywktLZ)F-D0 gHhZdz-)M`!&lveI>_gskV2Ckzy85}Sb4q9e0JU(RJpcdz diff --git a/runelite-client/src/test/java/net/runelite/client/util/StackFormatterTest.java b/runelite-client/src/test/java/net/runelite/client/util/StackFormatterTest.java index 9c344a909f..c36abcf8b3 100644 --- a/runelite-client/src/test/java/net/runelite/client/util/StackFormatterTest.java +++ b/runelite-client/src/test/java/net/runelite/client/util/StackFormatterTest.java @@ -26,12 +26,39 @@ package net.runelite.client.util; import java.text.NumberFormat; import java.text.ParseException; +import java.util.Locale; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; +import org.junit.Before; import org.junit.Test; public class StackFormatterTest { + @Before + public void setUp() + { + Locale.setDefault(Locale.ENGLISH); + } + + @Test + public void quantityToRSDecimalStackSize() + { + assertEquals("0", StackFormatter.quantityToRSDecimalStack(0)); + assertEquals("8500", StackFormatter.quantityToRSDecimalStack(8_500)); + assertEquals("10K", StackFormatter.quantityToRSDecimalStack(10_000)); + assertEquals("21,7K", StackFormatter.quantityToRSDecimalStack(21_700)); + assertEquals("100K", StackFormatter.quantityToRSDecimalStack(100_000)); + assertEquals("100,3K", StackFormatter.quantityToRSDecimalStack(100_300)); + assertEquals("1M", StackFormatter.quantityToRSDecimalStack(1_000_000)); + assertEquals("8,4M", StackFormatter.quantityToRSDecimalStack(8_450_000)); + assertEquals("10M", StackFormatter.quantityToRSDecimalStack(10_000_000)); + assertEquals("12,8M", StackFormatter.quantityToRSDecimalStack(12_800_000)); + assertEquals("100M", StackFormatter.quantityToRSDecimalStack(100_000_000)); + assertEquals("250,1M", StackFormatter.quantityToRSDecimalStack(250_100_000)); + assertEquals("1B", StackFormatter.quantityToRSDecimalStack(1_000_000_000)); + assertEquals("1,5B", StackFormatter.quantityToRSDecimalStack(1500_000_000)); + assertEquals("2,1B", StackFormatter.quantityToRSDecimalStack(Integer.MAX_VALUE)); + } @Test public void quantityToRSStackSize()