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
This commit is contained in:
Ruben Amendoeira
2018-04-22 04:24:46 +01:00
parent e56e559ecd
commit ab7e969320
13 changed files with 699 additions and 216 deletions

View File

@@ -1,5 +1,6 @@
/*
* Copyright (c) 2018, Seth <https://github.com/sethtroll>
* Copyright (c) 2018, Psikoi <https://github.com/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);
}
}
}

View File

@@ -1,5 +1,6 @@
/*
* Copyright (c) 2018, SomeoneWithAnInternetConnection
* Copyright (c) 2018, Psikoi <https://github.com/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 "<html><body style = 'color:" + SwingUtil.toHexColor(ColorScheme.LIGHT_GRAY_COLOR) + "'>Progress: <span style = 'color:white'>" + value + "</span></body></html>";
}
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 "<html><body style = 'color:white'>" + key + "<span style = 'color:" + SwingUtil.toHexColor(ColorScheme.LIGHT_GRAY_COLOR) + "'>" + value + "</span></body></html>";
}
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;
}
}

View File

@@ -0,0 +1,201 @@
/*
* Copyright (c) 2018, SomeoneWithAnInternetConnection
* Copyright (c) 2018, Psikoi <https://github.com/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);
}
}
}

View File

@@ -1,5 +1,6 @@
/*
* Copyright (c) 2018, SomeoneWithAnInternetConnection
* Copyright (c) 2018, Psikoi <https://github.com/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();
}
}
}

View File

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

View File

@@ -1,5 +1,6 @@
/*
* Copyright (c) 2018, Seth <https://github.com/sethtroll>
* Copyright (c) 2018, Psikoi <https://github.com/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<GrandExchangeItems> 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<GrandExchangeItems> 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();
}
}
}

View File

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

View File

@@ -128,6 +128,61 @@ public class StackFormatter
}
}
/**
* Convert a quantity to stack size as it would
* appear in RuneScape. (with decimals)
* <p>
* This differs from quantityToRSStack in that it displays
* decimals. Ex: 27100 is 27,1k (not 27k)
* <p>
* 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)

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 388 B

View File

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