diff --git a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java index d29593255e..e861861859 100644 --- a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java +++ b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetID.java @@ -54,6 +54,8 @@ public class WidgetID public static final int BA_COLLECTOR_GROUP_ID = 486; public static final int BA_DEFENDER_GROUP_ID = 487; public static final int BA_HEALER_GROUP_ID = 488; + public static final int LEVEL_UP_GROUP_ID = 233; + public static final int QUEST_COMPLETED_GROUP_ID = 277; static class WorldMap { @@ -248,4 +250,14 @@ public class WidgetID static final int ROLE_SPRITE = 6; static final int ROLE = 7; } + + static class LevelUp + { + static final int TEXT = 1; + } + + static class QuestCompleted + { + static final int NAME_TEXT = 2; + } } diff --git a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java index b73990fca2..7995627c8f 100644 --- a/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java +++ b/runelite-api/src/main/java/net/runelite/api/widgets/WidgetInfo.java @@ -152,7 +152,13 @@ public enum WidgetInfo BA_DEF_CALL_TEXT(WidgetID.BA_DEFENDER_GROUP_ID, WidgetID.BarbarianAssault.TO_CALL), BA_DEF_LISTEN_TEXT(WidgetID.BA_DEFENDER_GROUP_ID, WidgetID.BarbarianAssault.CORRECT_STYLE), BA_DEF_ROLE_TEXT(WidgetID.BA_DEFENDER_GROUP_ID, WidgetID.BarbarianAssault.ROLE), - BA_DEF_ROLE_SPRITE(WidgetID.BA_DEFENDER_GROUP_ID, WidgetID.BarbarianAssault.ROLE_SPRITE); + BA_DEF_ROLE_SPRITE(WidgetID.BA_DEFENDER_GROUP_ID, WidgetID.BarbarianAssault.ROLE_SPRITE), + + LEVEL_UP(WidgetID.LEVEL_UP_GROUP_ID, 0), + LEVEL_UP_TEXT(WidgetID.LEVEL_UP_GROUP_ID, WidgetID.LevelUp.TEXT), + + QUEST_COMPLETED(WidgetID.QUEST_COMPLETED_GROUP_ID, 0), + QUEST_COMPLETED_NAME_TEXT(WidgetID.QUEST_COMPLETED_GROUP_ID, WidgetID.QuestCompleted.NAME_TEXT); private final int groupId; private final int childId; diff --git a/runelite-client/pom.xml b/runelite-client/pom.xml index 78633aa483..80a21011a7 100644 --- a/runelite-client/pom.xml +++ b/runelite-client/pom.xml @@ -78,7 +78,7 @@ org.pushingpixels substance - 7.1.00-rc + 8.0.00-dev org.pushingpixels diff --git a/runelite-client/src/main/java/net/runelite/client/RuneLite.java b/runelite-client/src/main/java/net/runelite/client/RuneLite.java index b4069ee4ab..7fde52249e 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLite.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLite.java @@ -39,6 +39,7 @@ import joptsimple.OptionParser; import joptsimple.OptionSet; import lombok.extern.slf4j.Slf4j; import net.runelite.api.Client; +import net.runelite.client.events.ClientUILoaded; import net.runelite.client.account.SessionManager; import net.runelite.client.chat.ChatMessageManager; import net.runelite.client.config.ConfigManager; @@ -55,6 +56,7 @@ public class RuneLite public static final File RUNELITE_DIR = new File(System.getProperty("user.home"), ".runelite"); public static final File PROFILES_DIR = new File(RUNELITE_DIR, "profiles"); public static final File PLUGIN_DIR = new File(RUNELITE_DIR, "plugins"); + public static final File SCREENSHOT_DIR = new File(RUNELITE_DIR, "screenshots"); private static Injector injector; private static OptionSet options; @@ -164,6 +166,8 @@ public class RuneLite pluginManager.watch(); SwingUtilities.invokeAndWait(() -> gui.showWithChrome(runeliteConfig.enableCustomChrome())); + + eventBus.post(new ClientUILoaded()); } public void setGui(ClientUI gui) diff --git a/runelite-client/src/main/java/net/runelite/client/callback/Hooks.java b/runelite-client/src/main/java/net/runelite/client/callback/Hooks.java index 63e549811c..2ab5179f52 100644 --- a/runelite-client/src/main/java/net/runelite/client/callback/Hooks.java +++ b/runelite-client/src/main/java/net/runelite/client/callback/Hooks.java @@ -112,6 +112,8 @@ public class Hooks { log.warn("Error during overlay rendering", ex); } + + renderer.provideScreenshot(image); } public static void drawRegion(Region region, int var1, int var2, int var3, int var4, int var5, int var6) diff --git a/runelite-client/src/main/java/net/runelite/client/events/ClientUILoaded.java b/runelite-client/src/main/java/net/runelite/client/events/ClientUILoaded.java new file mode 100644 index 0000000000..dcff604949 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/events/ClientUILoaded.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2018, Adam + * 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.events; + +import lombok.Data; + +@Data +public class ClientUILoaded +{ +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/screenshot/ScreenshotConfig.java b/runelite-client/src/main/java/net/runelite/client/plugins/screenshot/ScreenshotConfig.java new file mode 100644 index 0000000000..cbf5a00492 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/screenshot/ScreenshotConfig.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2018, UniquePassive + * 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.screenshot; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigItem; + +@ConfigGroup( + keyName = "screenshot", + name = "Screenshot", + description = "Configuration for the Screenshot plugin" +) +public interface ScreenshotConfig extends Config +{ + @ConfigItem( + keyName = "includeFrame", + name = "Include Client Frame", + description = "Configures whether or not the client frame is included in screenshots", + position = 0 + ) + default boolean includeFrame() + { + return true; + } + + @ConfigItem( + keyName = "displayDate", + name = "Display Date", + description = "Configures whether or not the report button shows the date the screenshot was taken", + position = 1 + ) + default boolean displayDate() + { + return true; + } + + @ConfigItem( + keyName = "notifyWhenTaken", + name = "Notify When Taken", + description = "Configures whether or not you are notified when a screenshot has been taken", + position = 2 + ) + default boolean notifyWhenTaken() + { + return true; + } + + @ConfigItem( + keyName = "uploadScreenshot", + name = "Upload To Imgur", + description = "Configures whether or not screenshots are uploaded to Imgur and copied into your clipboard", + position = 3 + ) + default boolean uploadScreenshot() + { + return false; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/screenshot/ScreenshotPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/screenshot/ScreenshotPlugin.java new file mode 100644 index 0000000000..803c0f378b --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/screenshot/ScreenshotPlugin.java @@ -0,0 +1,365 @@ +/* + * Copyright (c) 2018, UniquePassive + * 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.screenshot; + +import net.runelite.client.plugins.screenshot.imgur.ImageUploadRequest; +import net.runelite.client.plugins.screenshot.imgur.ImageUploadResponse; +import com.google.common.eventbus.Subscribe; +import com.google.inject.Provides; +import java.awt.Color; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Point; +import java.awt.Toolkit; +import java.awt.TrayIcon; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.StringSelection; +import java.io.InputStreamReader; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.SwingUtilities; +import net.runelite.api.Client; +import net.runelite.client.events.ClientUILoaded; +import net.runelite.api.events.WidgetHiddenChanged; +import net.runelite.api.widgets.Widget; +import net.runelite.api.widgets.WidgetInfo; +import net.runelite.client.Notifier; +import net.runelite.client.RuneLite; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.ui.ClientUI; +import net.runelite.client.ui.FontManager; +import net.runelite.client.ui.overlay.OverlayRenderer; +import javax.imageio.ImageIO; +import javax.inject.Inject; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import lombok.extern.slf4j.Slf4j; +import net.runelite.http.api.RuneLiteAPI; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.pushingpixels.substance.internal.utils.SubstanceCoreUtilities; +import static net.runelite.api.widgets.WidgetID.LEVEL_UP_GROUP_ID; +import static net.runelite.api.widgets.WidgetID.QUEST_COMPLETED_GROUP_ID; +import static net.runelite.api.widgets.WidgetInfo.TO_GROUP; +import okhttp3.HttpUrl; + +@PluginDescriptor( + name = "Screenshot plugin" +) +@Slf4j +public class ScreenshotPlugin extends Plugin +{ + private static final String IMGUR_CLIENT_ID = "30d71e5f6860809"; + private static final HttpUrl IMGUR_IMAGE_UPLOAD_URL = HttpUrl.parse("https://api.imgur.com/3/image"); + private static final MediaType JSON = MediaType.parse("application/json"); + + private static final DateFormat DATE_FORMAT = new SimpleDateFormat("MMM. dd, yyyy", Locale.US); + private static final DateFormat TIME_FORMAT = new SimpleDateFormat("h.m.s a d MMM. yyyy", Locale.US); + + @Inject + ScreenshotConfig config; + + @Inject + Notifier notifier; + + @Inject + Client client; + + @Inject + ClientUI clientUi; + + @Inject + OverlayRenderer overlayRenderer; + + private JButton titleBarButton; + + @Provides + ScreenshotConfig getConfig(ConfigManager configManager) + { + return configManager.getConfig(ScreenshotConfig.class); + } + + @Override + protected void startUp() throws Exception + { + // prior to UI loading this does nothing + addButtonToTitleBar(); + } + + @Override + protected void shutDown() throws Exception + { + removeButtonFromTitlebar(); + } + + @Subscribe + public void clientUiLoaded(ClientUILoaded e) + { + addButtonToTitleBar(); + } + + private void addButtonToTitleBar() + { + try + { + BufferedImage iconImage = ImageIO.read(ScreenshotPlugin.class.getResourceAsStream("screenshot.png")); + BufferedImage invertedIconImage = ImageIO.read(ScreenshotPlugin.class.getResourceAsStream("screenshot_inverted.png")); + + SwingUtilities.invokeLater(() -> + { + titleBarButton = new JButton(); + titleBarButton.setToolTipText("Take screenshot"); + titleBarButton.addMouseListener(new MouseAdapter() + { + @Override + public void mouseClicked(MouseEvent e) + { + super.mouseClicked(e); + takeScreenshot(TIME_FORMAT.format(new Date()), client.getLocalPlayer() != null); + } + }); + + clientUi.addButtonToTitleBar(titleBarButton, iconImage, invertedIconImage, 130); + }); + } + catch (IOException ex) + { + log.warn("Error adding screenshot button to titlebar", ex); + } + } + + private void removeButtonFromTitlebar() + { + SwingUtilities.invokeLater(() -> + { + JComponent titleBar = SubstanceCoreUtilities.getTitlePaneComponent(clientUi); + titleBar.remove(titleBarButton); + + clientUi.revalidate(); + clientUi.repaint(); + }); + } + + @Subscribe + public void hideWidgets(WidgetHiddenChanged event) + { + Widget widget = event.getWidget(); + + if (widget.isHidden()) + { + return; + } + + String fileName; + + switch (TO_GROUP(widget.getId())) + { + case LEVEL_UP_GROUP_ID: + { + Widget textChild = client.getWidget(WidgetInfo.LEVEL_UP_TEXT); + + if (textChild == null) + { + return; + } + + // "Your Firemaking level is now 9." + String text = textChild.getText(); + + String skillName = text.substring(5, text.indexOf(" level")); + String skillLevel = text.substring(text.lastIndexOf(" ") + 1, text.length() - 1); + + fileName = skillName + " (" + skillLevel + ")"; + break; + } + case QUEST_COMPLETED_GROUP_ID: + { + Widget textChild = client.getWidget(WidgetInfo.QUEST_COMPLETED_NAME_TEXT); + + if (textChild == null) + { + return; + } + + // "You have completed The Corsair Curse!" + String text = textChild.getText(); + + fileName = text.substring(19, text.length() - 1); + break; + } + default: + return; + } + + takeScreenshot(fileName, config.displayDate()); + } + + private void takeScreenshot(String fileName, boolean displayDate) + { + overlayRenderer.requestScreenshot(image -> + { + BufferedImage screenshot = config.includeFrame() + ? new BufferedImage(clientUi.getWidth(), clientUi.getHeight(), BufferedImage.TYPE_INT_ARGB) + : new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB); + + Graphics graphics = screenshot.getGraphics(); + + int gameOffsetX = 0; + int gameOffsetY = 0; + + if (config.includeFrame()) + { + // Draw the client frame onto the screenshot + clientUi.paint(graphics); + + // Evaluate the position of the game inside the frame + Point gamePoint = SwingUtilities.convertPoint(client.getCanvas(), 0, 0, clientUi); + gameOffsetX = gamePoint.x; + gameOffsetY = gamePoint.y; + } + + // Draw the game onto the screenshot + graphics.drawImage(image, gameOffsetX, gameOffsetY, null); + + if (displayDate) + { + try (InputStream reportButton = ScreenshotPlugin.class.getResourceAsStream("report_button.png")) + { + BufferedImage reportButtonImage = ImageIO.read(reportButton); + + int x = gameOffsetX + 403; + int y = gameOffsetY + image.getHeight() - reportButtonImage.getHeight() - 1; + + graphics.drawImage(reportButtonImage, x, y, null); + + graphics.setFont(FontManager.getRunescapeSmallFont()); + FontMetrics fontMetrics = graphics.getFontMetrics(); + + String date = DATE_FORMAT.format(new Date()); + int dateWidth = fontMetrics.stringWidth(date); + int dateHeight = fontMetrics.getHeight(); + + int textX = x + reportButtonImage.getWidth() / 2 - dateWidth / 2; + int textY = y + reportButtonImage.getHeight() / 2 + dateHeight / 2; + + graphics.setColor(Color.BLACK); + graphics.drawString(date, textX + 1, textY + 1); + + graphics.setColor(Color.WHITE); + graphics.drawString(date, textX, textY); + } + catch (Exception ex) + { + log.warn("error displaying date on screenshot", ex); + } + } + + File playerFolder = RuneLite.SCREENSHOT_DIR; + + if (client.getLocalPlayer() != null) + { + playerFolder = new File(RuneLite.SCREENSHOT_DIR, client.getLocalPlayer().getName()); + } + + playerFolder.mkdirs(); + + try + { + File screenshotFile = new File(playerFolder, fileName + ".png"); + + ImageIO.write(screenshot, "PNG", screenshotFile); + + if (config.uploadScreenshot()) + { + uploadScreenshot(screenshotFile); + } + else if (config.notifyWhenTaken()) + { + notifier.notify("A screenshot was saved to " + screenshotFile, TrayIcon.MessageType.INFO); + } + } + catch (IOException ex) + { + log.warn("error writing screenshot", ex); + } + }); + } + + private void uploadScreenshot(File screenshotFile) throws IOException + { + String json = RuneLiteAPI.GSON.toJson(new ImageUploadRequest(screenshotFile)); + + Request request = new Request.Builder() + .url(IMGUR_IMAGE_UPLOAD_URL) + .addHeader("Authorization", "Client-ID " + IMGUR_CLIENT_ID) + .post(RequestBody.create(JSON, json)) + .build(); + + RuneLiteAPI.CLIENT.newCall(request).enqueue(new Callback() + { + @Override + public void onFailure(Call call, IOException ex) + { + log.warn("error uploading screenshot", ex); + } + + @Override + public void onResponse(Call call, Response response) throws IOException + { + InputStream in = response.body().byteStream(); + ImageUploadResponse imageUploadResponse = RuneLiteAPI.GSON + .fromJson(new InputStreamReader(in), ImageUploadResponse.class); + + if (imageUploadResponse.isSuccess()) + { + String link = imageUploadResponse.getData().getLink(); + + StringSelection selection = new StringSelection(link); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + clipboard.setContents(selection, selection); + + if (config.notifyWhenTaken()) + { + notifier.notify("A screenshot was uploaded and inserted into your clipboard!", TrayIcon.MessageType.INFO); + } + } + } + }); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/screenshot/imgur/ImageUploadRequest.java b/runelite-client/src/main/java/net/runelite/client/plugins/screenshot/imgur/ImageUploadRequest.java new file mode 100644 index 0000000000..91907b9158 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/screenshot/imgur/ImageUploadRequest.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2018, UniquePassive + * 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.screenshot.imgur; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Base64; +import lombok.Data; + +@Data +public class ImageUploadRequest +{ + private final String image; + private final String type; + + public ImageUploadRequest(File imageFile) throws IOException + { + this.image = Base64.getEncoder().encodeToString(Files.readAllBytes(imageFile.toPath())); + this.type = "base64"; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/screenshot/imgur/ImageUploadResponse.java b/runelite-client/src/main/java/net/runelite/client/plugins/screenshot/imgur/ImageUploadResponse.java new file mode 100644 index 0000000000..933fa86521 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/screenshot/imgur/ImageUploadResponse.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2018, UniquePassive + * 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.screenshot.imgur; + +import lombok.Data; + +@Data +public class ImageUploadResponse +{ + private Data data; + private boolean success; + + @lombok.Data + public static class Data + { + private String link; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java b/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java index 7756517b04..8e2f467351 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java @@ -30,19 +30,28 @@ import java.awt.AWTException; import java.awt.BorderLayout; import java.awt.Canvas; import java.awt.Cursor; +import java.awt.Desktop; import java.awt.Dimension; import java.awt.Frame; +import java.awt.Image; import java.awt.SystemTray; import java.awt.TrayIcon; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.image.BufferedImage; import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; import java.util.Enumeration; import javax.imageio.ImageIO; import javax.swing.BoxLayout; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.JPanel; @@ -59,13 +68,16 @@ import net.runelite.api.Client; import net.runelite.api.GameState; import net.runelite.client.RuneLiteProperties; import org.pushingpixels.substance.api.skin.SubstanceGraphiteLookAndFeel; -import org.pushingpixels.substance.internal.ui.SubstanceRootPaneUI; +import org.pushingpixels.substance.internal.SubstanceSynapse; +import org.pushingpixels.substance.internal.utils.SubstanceCoreUtilities; +import org.pushingpixels.substance.internal.utils.SubstanceTitlePaneUtilities; @Slf4j public class ClientUI extends JFrame { private static final int PANEL_EXPANDED_WIDTH = PluginPanel.PANEL_WIDTH + PluginPanel.SCROLLBAR_WIDTH; private static final BufferedImage ICON; + private static final String DISCORD_INVITE = "https://discord.gg/R4BQ8tU"; @Getter private TrayIcon trayIcon; @@ -150,7 +162,36 @@ public class ClientUI extends JFrame setLocationRelativeTo(getOwner()); if (customChrome) { - new TitleBarPane(this.getRootPane(), (SubstanceRootPaneUI) this.getRootPane().getUI()).editTitleBar(this); + try + { + BufferedImage discordIcon = ImageIO.read(ClientUI.class.getResourceAsStream("discord.png")); + BufferedImage invertedIcon = ImageIO.read(ClientUI.class.getResourceAsStream("discord_inverted.png")); + + JButton discordButton = new JButton(); + discordButton.setToolTipText("Join Discord"); + discordButton.addMouseListener(new MouseAdapter() + { + @Override + public void mouseClicked(MouseEvent e) + { + super.mouseClicked(e); + try + { + Desktop.getDesktop().browse(new URL(DISCORD_INVITE).toURI()); + } + catch (IOException | URISyntaxException ex) + { + log.warn("error opening browser", ex); + } + } + }); + + addButtonToTitleBar(discordButton, discordIcon, invertedIcon, 100); + } + catch (IOException ex) + { + log.warn("unable to load discord button", ex); + } } setVisible(true); @@ -172,6 +213,44 @@ public class ClientUI extends JFrame } } + public void addButtonToTitleBar(JButton button, Image iconImage, Image invertedIconImage, int xOffset) + { + JComponent titleBar = SubstanceCoreUtilities.getTitlePaneComponent(this); + + if (titleBar == null) + { + return; + } + + int size = titleBar.getHeight() - 6; + + ImageIcon icon = new ImageIcon(iconImage.getScaledInstance(size, size, Image.SCALE_SMOOTH)); + ImageIcon invertedIcon = new ImageIcon(invertedIconImage.getScaledInstance(size, size, Image.SCALE_SMOOTH)); + + button.setIcon(icon); + button.setRolloverIcon(invertedIcon); + button.putClientProperty(SubstanceSynapse.FLAT_LOOK, Boolean.TRUE); + button.putClientProperty(SubstanceTitlePaneUtilities.EXTRA_COMPONENT_KIND, SubstanceTitlePaneUtilities.ExtraComponentKind.TRAILING); + button.setFocusable(false); + button.setBounds(titleBar.getWidth() - xOffset, 2, + icon.getIconWidth() + 4, icon.getIconHeight() + 2); + + titleBar.addComponentListener(new ComponentAdapter() + { + @Override + public void componentResized(ComponentEvent e) + { + super.componentResized(e); + button.setBounds(titleBar.getWidth() - xOffset, 1, button.getWidth(), button.getHeight()); + } + }); + + titleBar.add(button); + + revalidate(); + repaint(); + } + private static void setUIFont(FontUIResource f) { final Enumeration keys = UIManager.getDefaults().keys(); diff --git a/runelite-client/src/main/java/net/runelite/client/ui/TitleBarPane.java b/runelite-client/src/main/java/net/runelite/client/ui/TitleBarPane.java deleted file mode 100644 index c4002f9a20..0000000000 --- a/runelite-client/src/main/java/net/runelite/client/ui/TitleBarPane.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (c) 2017. l2- - * - * 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.ui; - -import java.awt.Desktop; -import java.awt.Image; -import java.awt.event.ComponentAdapter; -import java.awt.event.ComponentEvent; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.image.BufferedImage; -import java.io.IOException; -import java.net.URISyntaxException; -import java.net.URL; -import javax.imageio.ImageIO; -import javax.swing.ImageIcon; -import javax.swing.JButton; -import javax.swing.JComponent; -import javax.swing.JFrame; -import javax.swing.JRootPane; -import lombok.extern.slf4j.Slf4j; -import org.pushingpixels.substance.api.SubstanceLookAndFeel; -import org.pushingpixels.substance.internal.ui.SubstanceRootPaneUI; -import org.pushingpixels.substance.internal.utils.SubstanceTitlePane; - -@Slf4j -public class TitleBarPane extends SubstanceTitlePane -{ - private static final String DISCORD_INVITE = "https://discord.gg/R4BQ8tU"; - - private BufferedImage discordIcon; - private BufferedImage invertedIcon; - - public TitleBarPane(JRootPane root, SubstanceRootPaneUI ui) - { - super(root, ui); - - try - { - discordIcon = ImageIO.read(ClientUI.class.getResourceAsStream("discord.png")); - invertedIcon = ImageIO.read(ClientUI.class.getResourceAsStream("discord_inverted.png")); - } - catch (IOException ex) - { - log.warn("unable to load discord icons", ex); - } - } - - public void editTitleBar(JFrame frame) - { - if (discordIcon == null || invertedIcon == null) - { - return; - } - - JComponent titleBar = SubstanceLookAndFeel.getTitlePaneComponent(frame); - - JButton discordButton = new JButton(); - int heigth = titleBar.getHeight() - 3; - int width = heigth; - int x = titleBar.getWidth() - 100; - int y = 1; - ImageIcon icon = new ImageIcon(discordIcon.getScaledInstance(width - 3, heigth - 3, Image.SCALE_SMOOTH)); - ImageIcon invIcon = new ImageIcon(invertedIcon.getScaledInstance(width - 3, heigth - 3, Image.SCALE_SMOOTH)); - - discordButton.setIcon(icon); - discordButton.setRolloverIcon(invIcon); - discordButton.putClientProperty("substancelaf.componentFlat", Boolean.TRUE); - discordButton.putClientProperty("substancelaf.internal.titlePane.extraComponentKind", ExtraComponentKind.TRAILING); - discordButton.setFocusable(false); - discordButton.setBounds(x, y, icon.getIconWidth() + 4, icon.getIconHeight() + 2); - discordButton.setToolTipText("Join Discord"); - - discordButton.addMouseListener(new MouseAdapter() - { - @Override - public void mouseClicked(MouseEvent e) - { - super.mouseClicked(e); - try - { - Desktop.getDesktop().browse(new URL(DISCORD_INVITE).toURI()); - } - catch (IOException | URISyntaxException ex) - { - log.warn("error opening browser", ex); - } - } - }); - - titleBar.addComponentListener(new ComponentAdapter() - { - @Override - public void componentResized(ComponentEvent e) - { - super.componentResized(e); - discordButton.setBounds(titleBar.getWidth() - 100, y, discordButton.getWidth(), discordButton.getHeight()); - } - }); - - titleBar.add(discordButton); - } -} diff --git a/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayRenderer.java b/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayRenderer.java index ae5d627b1a..a982962fca 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayRenderer.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayRenderer.java @@ -34,11 +34,14 @@ import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.inject.Inject; import javax.inject.Provider; import javax.inject.Singleton; +import lombok.extern.slf4j.Slf4j; import net.runelite.api.Client; import net.runelite.api.GameState; import net.runelite.api.widgets.Widget; @@ -51,6 +54,7 @@ import net.runelite.client.ui.overlay.infobox.InfoBoxOverlay; import net.runelite.client.ui.overlay.tooltip.TooltipOverlay; @Singleton +@Slf4j public class OverlayRenderer { private static final int BORDER_TOP = 25; @@ -75,6 +79,8 @@ public class OverlayRenderer private BufferedImage surface; private Graphics2D surfaceGraphics; + private ConcurrentLinkedQueue> screenshotRequests = new ConcurrentLinkedQueue<>(); + @Subscribe public void onResizableChanged(ResizeableChanged event) { @@ -299,4 +305,25 @@ public class OverlayRenderer subGraphics.dispose(); return dimension; } + + public void provideScreenshot(BufferedImage image) + { + Consumer consumer; + while ((consumer = screenshotRequests.poll()) != null) + { + try + { + consumer.accept(image); + } + catch (Exception ex) + { + log.warn("error in screenshot callback", ex); + } + } + } + + public void requestScreenshot(Consumer consumer) + { + screenshotRequests.add(consumer); + } } diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/screenshot/report_button.png b/runelite-client/src/main/resources/net/runelite/client/plugins/screenshot/report_button.png new file mode 100644 index 0000000000..34603643c6 Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/screenshot/report_button.png differ diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/screenshot/screenshot.png b/runelite-client/src/main/resources/net/runelite/client/plugins/screenshot/screenshot.png new file mode 100644 index 0000000000..9a4f892bd7 Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/screenshot/screenshot.png differ diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/screenshot/screenshot_inverted.png b/runelite-client/src/main/resources/net/runelite/client/plugins/screenshot/screenshot_inverted.png new file mode 100644 index 0000000000..37a546d555 Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/plugins/screenshot/screenshot_inverted.png differ