From 0c5cde089400bcaf9ea05bdde0a07113fe049c2d Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 17 Mar 2018 01:51:28 +0100 Subject: [PATCH 1/4] Add support for points to ConfigManager Signed-off-by: Tomas Slusny --- .../net/runelite/client/config/ConfigManager.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java index 9b7062121d..0078d8e470 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java +++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java @@ -28,6 +28,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.eventbus.EventBus; import java.awt.Color; import java.awt.Dimension; +import java.awt.Point; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -420,6 +421,13 @@ public class ConfigManager int height = Integer.parseInt(splitStr[1]); return new Dimension(width, height); } + if (type == Point.class) + { + String[] splitStr = str.split(":"); + int width = Integer.parseInt(splitStr[0]); + int height = Integer.parseInt(splitStr[1]); + return new Point(width, height); + } if (type.isEnum()) { return Enum.valueOf((Class) type, str); @@ -442,6 +450,11 @@ public class ConfigManager Dimension d = (Dimension) object; return d.width + "x" + d.height; } + if (object instanceof Point) + { + Point p = (Point) object; + return p.x + ":" + p.y; + } return object.toString(); } } From 10629bfa4a5c8d1d1f260f9f85ad271219680cdd Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 19 Mar 2018 19:16:53 +0100 Subject: [PATCH 2/4] Add support for getting real canvas dimensions In stretched mode the canvas dimensions are the stretched dimensions, so add method that will return normal game dimensions in case stretched mode is enabled and we are not in resizeable mode. Signed-off-by: Tomas Slusny --- .../src/main/java/net/runelite/api/Client.java | 2 ++ .../net/runelite/mixins/StretchedFixedModeMixin.java | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/runelite-api/src/main/java/net/runelite/api/Client.java b/runelite-api/src/main/java/net/runelite/api/Client.java index 62a67f510b..f2b1f60a00 100644 --- a/runelite-api/src/main/java/net/runelite/api/Client.java +++ b/runelite-api/src/main/java/net/runelite/api/Client.java @@ -331,6 +331,8 @@ public interface Client extends GameEngine Dimension getStretchedDimensions(); + Dimension getRealDimensions(); + /** * Changes world. Works only on login screen * @param world world diff --git a/runelite-mixins/src/main/java/net/runelite/mixins/StretchedFixedModeMixin.java b/runelite-mixins/src/main/java/net/runelite/mixins/StretchedFixedModeMixin.java index 3550defaa6..9729ef676d 100644 --- a/runelite-mixins/src/main/java/net/runelite/mixins/StretchedFixedModeMixin.java +++ b/runelite-mixins/src/main/java/net/runelite/mixins/StretchedFixedModeMixin.java @@ -85,6 +85,18 @@ public abstract class StretchedFixedModeMixin implements RSClient cachedStretchedDimensions = null; } + @Inject + @Override + public Dimension getRealDimensions() + { + if (isStretchedEnabled() && !isResized()) + { + return Constants.GAME_FIXED_SIZE; + } + + return getCanvas().getSize(); + } + @Inject @Override public Dimension getStretchedDimensions() From 91c39258675c6ae2bb4aae19e3564802b4189c70 Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 22 Mar 2018 21:06:13 -0400 Subject: [PATCH 3/4] config manager: add get and set configuration methods for objects --- .../runelite/client/config/ConfigManager.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java index 0078d8e470..f9ec3b978b 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java +++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java @@ -24,6 +24,7 @@ */ package net.runelite.client.config; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.common.eventbus.EventBus; import java.awt.Color; @@ -245,6 +246,16 @@ public class ConfigManager return properties.getProperty(groupName + "." + key); } + public T getConfiguration(String groupName, String key, Class clazz) + { + String value = getConfiguration(groupName, key); + if (!Strings.isNullOrEmpty(value)) + { + return (T) stringToObject(value, clazz); + } + return null; + } + public void setConfiguration(String groupName, String key, String value) { log.debug("Setting configuration value for {}.{} to {}", groupName, key, value); @@ -290,6 +301,11 @@ public class ConfigManager eventBus.post(configChanged); } + public void setConfiguration(String groupName, String key, Object value) + { + setConfiguration(groupName, key, objectToString(value)); + } + public void unsetConfiguration(String groupName, String key) { log.debug("Unsetting configuration value for {}.{}", groupName, key); From add6db5d7ee7288c9715475a4932de57cf5888c3 Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 22 Mar 2018 21:10:35 -0400 Subject: [PATCH 4/4] Add movable overlay support This overhauls most of the overlay renderer to do so. There are 4 (or 5) "snap corners" that let overlays change their position, or you can set a custom location which is at a set x/y. Calculate which layer overlays are on up front and build separate lists for each layer. The fixed-point under-widget overlays are moved to above-widget so that they work when moved above the interfaces. --- .../runelite/client/ui/overlay/Overlay.java | 5 + .../client/ui/overlay/OverlayBounds.java | 104 ++++ .../client/ui/overlay/OverlayRenderer.java | 586 ++++++++++++++---- .../client/ui/overlay/OverlayUtil.java | 30 + 4 files changed, 590 insertions(+), 135 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayBounds.java diff --git a/runelite-client/src/main/java/net/runelite/client/ui/overlay/Overlay.java b/runelite-client/src/main/java/net/runelite/client/ui/overlay/Overlay.java index cfc25313b9..4b6c7b9eca 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/overlay/Overlay.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/overlay/Overlay.java @@ -24,11 +24,16 @@ */ package net.runelite.client.ui.overlay; +import java.awt.Point; +import java.awt.Rectangle; import lombok.Data; @Data public abstract class Overlay implements RenderableEntity { + private Point preferredLocation; + private OverlayPosition preferredPosition; + private Rectangle bounds = new Rectangle(); private OverlayPosition position = OverlayPosition.TOP_LEFT; private OverlayPriority priority = OverlayPriority.NONE; private OverlayLayer layer = OverlayLayer.UNDER_WIDGETS; diff --git a/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayBounds.java b/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayBounds.java new file mode 100644 index 0000000000..e81b1807fd --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayBounds.java @@ -0,0 +1,104 @@ +/* + * 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.ui.overlay; + +import java.awt.Rectangle; +import java.util.Arrays; +import java.util.Collection; +import lombok.AllArgsConstructor; +import lombok.Value; +import static net.runelite.client.ui.overlay.OverlayPosition.ABOVE_CHATBOX_RIGHT; +import static net.runelite.client.ui.overlay.OverlayPosition.BOTTOM_LEFT; +import static net.runelite.client.ui.overlay.OverlayPosition.BOTTOM_RIGHT; +import static net.runelite.client.ui.overlay.OverlayPosition.TOP_LEFT; +import static net.runelite.client.ui.overlay.OverlayPosition.TOP_RIGHT; + +@AllArgsConstructor +@Value +class OverlayBounds +{ + private final Rectangle topLeft, topRight, bottomLeft, bottomRight, aboveChatboxRight; + + OverlayBounds(OverlayBounds other) + { + topLeft = new Rectangle(other.topLeft); + topRight = new Rectangle(other.topRight); + bottomLeft = new Rectangle(other.bottomLeft); + bottomRight = new Rectangle(other.bottomRight); + aboveChatboxRight = new Rectangle(other.aboveChatboxRight); + } + + Rectangle forPosition(OverlayPosition overlayPosition) + { + switch (overlayPosition) + { + case TOP_LEFT: + return topLeft; + case TOP_RIGHT: + return topRight; + case BOTTOM_LEFT: + return bottomLeft; + case BOTTOM_RIGHT: + return bottomRight; + case ABOVE_CHATBOX_RIGHT: + return aboveChatboxRight; + default: + throw new IllegalArgumentException(); + } + } + + OverlayPosition fromBounds(Rectangle bounds) + { + if (bounds == topLeft) + { + return TOP_LEFT; + } + else if (bounds == topRight) + { + return TOP_RIGHT; + } + else if (bounds == bottomLeft) + { + return BOTTOM_LEFT; + } + else if (bounds == bottomRight) + { + return BOTTOM_RIGHT; + } + else if (bounds == aboveChatboxRight) + { + return ABOVE_CHATBOX_RIGHT; + } + else + { + throw new IllegalArgumentException(); + } + } + + Collection getBounds() + { + return Arrays.asList(topLeft, topRight, bottomLeft, bottomRight, aboveChatboxRight); + } +} 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 c4c55dfdb1..0923d2dbab 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 @@ -31,6 +31,8 @@ import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.util.List; import java.util.Objects; @@ -42,21 +44,28 @@ import java.util.stream.Stream; import javax.inject.Inject; import javax.inject.Provider; import javax.inject.Singleton; +import javax.swing.SwingUtilities; import lombok.extern.slf4j.Slf4j; import net.runelite.api.Client; import net.runelite.api.GameState; import net.runelite.api.events.GameStateChanged; -import net.runelite.api.events.ResizeableChanged; import net.runelite.api.widgets.Widget; import net.runelite.api.widgets.WidgetInfo; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.config.RuneLiteConfig; import net.runelite.client.events.PluginChanged; +import net.runelite.client.input.KeyListener; +import net.runelite.client.input.KeyManager; +import net.runelite.client.input.MouseListener; +import net.runelite.client.input.MouseManager; import net.runelite.client.plugins.PluginManager; import net.runelite.client.ui.overlay.infobox.InfoBoxOverlay; import net.runelite.client.ui.overlay.tooltip.TooltipOverlay; @Singleton @Slf4j -public class OverlayRenderer +public class OverlayRenderer extends MouseListener implements KeyListener { private static final int BORDER_LEFT_RESIZABLE = 5; private static final int BORDER_TOP_RESIZABLE = 20; @@ -66,29 +75,56 @@ public class OverlayRenderer private static final int BORDER_RIGHT = 2; private static final int BORDER_BOTTOM = 2; private static final int PADDING = 2; + private static final Dimension SNAP_CORNER_SIZE = new Dimension(80, 80); + private static final Color SNAP_CORNER_COLOR = new Color(0, 255, 255, 50); + private static final Color SNAP_CORNER_ACTIVE_COLOR = new Color(0, 255, 0, 100); + private static final Color MOVING_OVERLAY_COLOR = new Color(255, 255, 0, 100); + private static final Color MOVING_OVERLAY_ACTIVE_COLOR = new Color(255, 255, 0, 200); + private static final String OVERLAY_CONFIG_PREFERRED_LOCATION = "_preferredLocation"; + private static final String OVERLAY_CONFIG_PREFERRED_POSITION = "_preferredPosition"; + + private final PluginManager pluginManager; + private final Provider clientProvider; + private final InfoBoxOverlay infoBoxOverlay; + private final ConfigManager configManager; + private final TooltipOverlay tooltipOverlay; + private final List allOverlays = new CopyOnWriteArrayList<>(); + private final List overlaysAboveScene = new CopyOnWriteArrayList<>(), + overlaysUnderWidgets = new CopyOnWriteArrayList<>(), + overlaysAboveWidgets = new CopyOnWriteArrayList<>(), + overlaysOnTop = new CopyOnWriteArrayList<>(); + private final ConcurrentLinkedQueue> screenshotRequests = new ConcurrentLinkedQueue<>(); + private final String runeliteGroupName = RuneLiteConfig.class.getAnnotation(ConfigGroup.class).keyName(); + + // Overlay movement variables + private final Point overlayOffset = new Point(); + private final Point mousePosition = new Point(); + private Overlay movedOverlay; + private boolean inOverlayDraggingMode; + + // Overlay state validation + private Rectangle viewportBounds; + private boolean chatboxHidden; + private boolean isResizeable; + private OverlayBounds snapCorners; @Inject - PluginManager pluginManager; - - @Inject - Provider clientProvider; - - @Inject - InfoBoxOverlay infoBoxOverlay; - - @Inject - TooltipOverlay tooltipOverlay; - - private final List overlays = new CopyOnWriteArrayList<>(); - private BufferedImage surface; - private Graphics2D surfaceGraphics; - - private ConcurrentLinkedQueue> screenshotRequests = new ConcurrentLinkedQueue<>(); - - @Subscribe - public void onResizableChanged(ResizeableChanged event) + private OverlayRenderer( + final Provider clientProvider, + final PluginManager pluginManager, + final MouseManager mouseManager, + final KeyManager keyManager, + final TooltipOverlay tooltipOverlay, + final InfoBoxOverlay infoBoxOverlay, + final ConfigManager configManager) { - updateSurface(); + this.clientProvider = clientProvider; + this.pluginManager = pluginManager; + this.tooltipOverlay = tooltipOverlay; + this.infoBoxOverlay = infoBoxOverlay; + this.configManager = configManager; + keyManager.registerKeyListener(this); + mouseManager.registerMouseListener(this); } @Subscribe @@ -101,32 +137,101 @@ public class OverlayRenderer return; } - if (event.getGameState().equals(GameState.LOGIN_SCREEN) || event.getGameState().equals(GameState.LOGGED_IN)) + if (event.getGameState().equals(GameState.LOGGED_IN)) { - refreshPlugins(); - updateSurface(); + rebuildOverlays(); } } @Subscribe public void onPluginChanged(PluginChanged event) { - refreshPlugins(); + rebuildOverlays(); } - private void refreshPlugins() + private List getOverlaysForLayer(OverlayLayer layer) { - overlays.clear(); - overlays.addAll(Stream + switch (layer) + { + case ABOVE_SCENE: + return overlaysAboveScene; + case UNDER_WIDGETS: + return overlaysUnderWidgets; + case ABOVE_WIDGETS: + return overlaysAboveWidgets; + case ALWAYS_ON_TOP: + return overlaysOnTop; + default: + throw new IllegalStateException(); + } + } + + private void rebuildOverlays() + { + List overlays = Stream .concat( pluginManager.getPlugins() .stream() - .filter(plugin -> pluginManager.isPluginEnabled(plugin)) + .filter(pluginManager::isPluginEnabled) .flatMap(plugin -> plugin.getOverlays().stream()), Stream.of(infoBoxOverlay, tooltipOverlay)) .filter(Objects::nonNull) - .collect(Collectors.toList())); + .collect(Collectors.toList()); + sortOverlays(overlays); + + allOverlays.clear(); + allOverlays.addAll(overlays); + + final Client client = clientProvider.get(); + + for (final Overlay overlay : overlays) + { + final Point location = loadOverlayLocation(overlay); + + if (location != null + && client.getCanvas() != null + && !client.getCanvas().contains(location)) + { + overlay.setPreferredLocation(null); + saveOverlayLocation(overlay); + } + else + { + overlay.setPreferredLocation(location); + } + + final OverlayPosition position = loadOverlayPosition(overlay); + overlay.setPreferredPosition(position); + } + + rebuildOverlayLayers(); + } + + private void rebuildOverlayLayers() + { + overlaysAboveScene.clear(); + overlaysUnderWidgets.clear(); + overlaysAboveWidgets.clear(); + overlaysOnTop.clear(); + + for (final Overlay overlay : allOverlays) + { + OverlayLayer layer = overlay.getLayer(); + + if (overlay.getPreferredLocation() != null && overlay.getPreferredPosition() == null) + { + // When UNDER_WIDGET overlays are in preferred locations, move to + // ABOVE_WIDGETS so that it can draw over interfaces + if (layer == OverlayLayer.UNDER_WIDGETS) + { + layer = OverlayLayer.ABOVE_WIDGETS; + } + } + + List overlayLayer = getOverlaysForLayer(layer); + overlayLayer.add(overlay); + } } static void sortOverlays(List overlays) @@ -151,89 +256,54 @@ public class OverlayRenderer }); } - private void updateSurface() + public void render(Graphics2D graphics, final OverlayLayer layer) { final Client client = clientProvider.get(); + List overlays = getOverlaysForLayer(layer); - if (client == null) + if (client == null + || overlays.isEmpty() + || client.getViewportWidget() == null + || client.getGameState() != GameState.LOGGED_IN + || client.getWidget(WidgetInfo.LOGIN_CLICK_TO_PLAY_SCREEN) != null) { return; } - final Dimension size = client.getCanvas().getSize(); - final BufferedImage temp = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB); - final Graphics2D subGraphics = temp.createGraphics(); - subGraphics.setBackground(new Color(0, 0, 0, 0)); - OverlayUtil.setGraphicProperties(subGraphics); - - surface = temp; - - if (surfaceGraphics != null) + if (shouldInvalidateOverlays()) { - surfaceGraphics.dispose(); + snapCorners = buildSnapCorners(); } - surfaceGraphics = subGraphics; - } - - public void render(Graphics2D graphics, OverlayLayer layer) - { - final Client client = clientProvider.get(); - - if (client == null || surface == null || overlays.isEmpty()) - { - return; - } - - if (client.getGameState() != GameState.LOGGED_IN) - { - return; - } - - if (client.getWidget(WidgetInfo.LOGIN_CLICK_TO_PLAY_SCREEN) != null) - { - return; - } - - final boolean isResizeable = client.isResized(); - final Widget viewport = client.getViewportWidget(); - final Rectangle bounds = viewport != null - ? new Rectangle(viewport.getBounds()) - : new Rectangle(0, 0, surface.getWidth(), surface.getHeight()); - - final Widget chatbox = client.getWidget(WidgetInfo.CHATBOX_MESSAGES); - final Rectangle chatboxBounds = chatbox != null - ? chatbox.getBounds() : new Rectangle(0, bounds.height, 519, 165); + // Create copy of snap corners because overlays will modify them + OverlayBounds snapCorners = new OverlayBounds(this.snapCorners); OverlayUtil.setGraphicProperties(graphics); - final Point topLeftPoint = new Point(); - topLeftPoint.move( - isResizeable ? BORDER_LEFT_RESIZABLE : BORDER_LEFT_FIXED, - isResizeable ? BORDER_TOP_RESIZABLE : BORDER_TOP_FIXED); - final Point topRightPoint = new Point(); - topRightPoint.move(bounds.x + bounds.width - BORDER_RIGHT, BORDER_TOP_FIXED); - final Point bottomLeftPoint = new Point(); - bottomLeftPoint.move(isResizeable ? BORDER_LEFT_RESIZABLE : BORDER_LEFT_FIXED, bounds.y + bounds.height - BORDER_BOTTOM); - final Point bottomRightPoint = new Point(); - bottomRightPoint.move(bounds.x + bounds.width - BORDER_RIGHT, bounds.y + bounds.height - BORDER_BOTTOM); - final Point rightChatboxPoint = new Point(); - rightChatboxPoint.move(bounds.x + chatboxBounds.width - BORDER_RIGHT, bounds.y + bounds.height - BORDER_BOTTOM); - //check to see if Chatbox is minimized - if (chatbox != null && isResizeable && chatbox.isHidden()) + // Draw snap corners + if (layer == OverlayLayer.UNDER_WIDGETS && movedOverlay != null) { - rightChatboxPoint.y += chatboxBounds.height; - bottomLeftPoint.y += chatboxBounds.height; + final OverlayBounds translatedSnapCorners = translateSnapCorners(snapCorners); + final Color previous = graphics.getColor(); + + for (final Rectangle corner : translatedSnapCorners.getBounds()) + { + graphics.setColor(corner.contains(mousePosition) ? SNAP_CORNER_ACTIVE_COLOR : SNAP_CORNER_COLOR); + graphics.fill(corner); + } + + graphics.setColor(previous); } for (Overlay overlay : overlays) { - if (overlay.getLayer() != layer) + OverlayPosition overlayPosition = overlay.getPosition(); + + if (overlay.getPreferredPosition() != null) { - continue; + overlayPosition = overlay.getPreferredPosition(); } - OverlayPosition overlayPosition = overlay.getPosition(); if (overlayPosition == OverlayPosition.ABOVE_CHATBOX_RIGHT && !client.isResized()) { // On fixed mode, ABOVE_CHATBOX_RIGHT is in the same location as @@ -241,25 +311,6 @@ public class OverlayRenderer // drawing over each other. overlayPosition = OverlayPosition.BOTTOM_RIGHT; } - final Point subPosition = new Point(); - switch (overlayPosition) - { - case BOTTOM_LEFT: - subPosition.setLocation(bottomLeftPoint); - break; - case BOTTOM_RIGHT: - subPosition.setLocation(bottomRightPoint); - break; - case TOP_LEFT: - subPosition.setLocation(topLeftPoint); - break; - case TOP_RIGHT: - subPosition.setLocation(topRightPoint); - break; - case ABOVE_CHATBOX_RIGHT: - subPosition.setLocation(rightChatboxPoint); - break; - } if (overlayPosition == OverlayPosition.DYNAMIC || overlayPosition == OverlayPosition.TOOLTIP) { @@ -267,48 +318,313 @@ public class OverlayRenderer } else { - final Dimension dimension = MoreObjects.firstNonNull(safeRender(overlay, surfaceGraphics, subPosition), new Dimension()); - if (dimension.width == 0 && dimension.height == 0) + final Point location = overlay.getBounds().getLocation(); + final Dimension dimension = overlay.getBounds().getSize(); + + // If the final position is not modified, layout it + if (overlay.getPreferredLocation() == null || overlay.getPreferredPosition() != null) + { + final Rectangle snapCorner = snapCorners.forPosition(overlayPosition); + final Point translation = OverlayUtil.transformPosition(overlayPosition, dimension); + location.setLocation(snapCorner.getX() + translation.x, snapCorner.getY() + translation.y); + final Point padding = OverlayUtil.padPosition(overlayPosition, dimension, PADDING); + snapCorner.translate(padding.x, padding.y); + } + else + { + location.setLocation(overlay.getPreferredLocation()); + } + + final Dimension overlayDimension = MoreObjects.firstNonNull( + safeRender(overlay, graphics, location), + new Dimension()); + + overlay.setBounds(new Rectangle(location, overlayDimension)); + + if (overlayDimension.width == 0 && overlayDimension.height == 0) { continue; } - final BufferedImage clippedImage = surface.getSubimage(0, 0, dimension.width, dimension.height); - - switch (overlayPosition) + if (inOverlayDraggingMode) { - case BOTTOM_LEFT: - bottomLeftPoint.x += dimension.width + (dimension.width == 0 ? 0 : PADDING); - break; - case BOTTOM_RIGHT: - bottomRightPoint.x -= dimension.width + (dimension.width == 0 ? 0 : PADDING); - break; - case TOP_LEFT: - topLeftPoint.y += dimension.height + (dimension.height == 0 ? 0 : PADDING); - break; - case TOP_RIGHT: - topRightPoint.y += dimension.height + (dimension.height == 0 ? 0 : PADDING); - break; - case ABOVE_CHATBOX_RIGHT: - rightChatboxPoint.y -= dimension.height + (dimension.height == 0 ? 0 : PADDING); - break; + final Color previous = graphics.getColor(); + graphics.setColor(movedOverlay == overlay ? MOVING_OVERLAY_ACTIVE_COLOR : MOVING_OVERLAY_COLOR); + graphics.drawRect(location.x, location.y, dimension.width - 1, dimension.height - 1); + graphics.setColor(previous); + } + } + } + } + + @Override + public MouseEvent mousePressed(MouseEvent mouseEvent) + { + if (!inOverlayDraggingMode) + { + return mouseEvent; + } + + final Point mousePoint = mouseEvent.getPoint(); + mousePosition.setLocation(mousePoint); + + for (Overlay overlay : allOverlays) + { + if (overlay.getBounds().contains(mousePoint)) + { + if (SwingUtilities.isRightMouseButton(mouseEvent)) + { + overlay.setPreferredLocation(null); + overlay.setPreferredPosition(null); + rebuildOverlayLayers(); + } + else + { + mousePoint.translate(-overlay.getBounds().x, -overlay.getBounds().y); + overlayOffset.setLocation(mousePoint); + movedOverlay = overlay; } - final Point transformed = OverlayUtil.transformPosition(overlayPosition, dimension); - graphics.drawImage(clippedImage, subPosition.x + transformed.x, subPosition.y + transformed.y, null); - surfaceGraphics.clearRect(0, 0, (int) dimension.getWidth(), (int) dimension.getHeight()); + mouseEvent.consume(); + break; } } + + return mouseEvent; + } + + @Override + public MouseEvent mouseDragged(MouseEvent mouseEvent) + { + if (!inOverlayDraggingMode) + { + return mouseEvent; + } + + final Client client = clientProvider.get(); + + if (client == null) + { + return mouseEvent; + } + + final Point mousePoint = mouseEvent.getPoint(); + mousePosition.setLocation(mousePoint); + final Rectangle canvasRect = new Rectangle(client.getRealDimensions()); + + if (!canvasRect.contains(mousePoint)) + { + return mouseEvent; + } + + if (movedOverlay != null) + { + mousePoint.translate(-overlayOffset.x, -overlayOffset.y); + movedOverlay.setPreferredPosition(null); + movedOverlay.setPreferredLocation(mousePoint); + rebuildOverlayLayers(); + mouseEvent.consume(); + } + + return mouseEvent; + } + + @Override + public MouseEvent mouseReleased(MouseEvent mouseEvent) + { + if (movedOverlay != null) + { + mousePosition.setLocation(-1, -1); + final OverlayBounds snapCorners = translateSnapCorners(buildSnapCorners()); + + for (Rectangle snapCorner : snapCorners.getBounds()) + { + if (snapCorner.contains(mouseEvent.getPoint())) + { + OverlayPosition position = snapCorners.fromBounds(snapCorner); + if (position == movedOverlay.getPosition()) + { + // overlay moves back to default position + position = null; + } + movedOverlay.setPreferredPosition(position); + movedOverlay.setPreferredLocation(null); // from dragging + break; + } + } + + saveOverlayPosition(movedOverlay); + saveOverlayLocation(movedOverlay); + rebuildOverlayLayers(); + movedOverlay = null; + mouseEvent.consume(); + } + + return mouseEvent; + } + + @Override + public void keyTyped(KeyEvent e) + { + } + + @Override + public void keyPressed(KeyEvent e) + { + if (e.isAltDown()) + { + inOverlayDraggingMode = true; + } + } + + @Override + public void keyReleased(KeyEvent e) + { + if (!e.isAltDown()) + { + inOverlayDraggingMode = false; + } } private Dimension safeRender(RenderableEntity entity, Graphics2D graphics, Point point) { final Graphics2D subGraphics = (Graphics2D) graphics.create(); + subGraphics.translate(point.x, point.y); final Dimension dimension = entity.render(subGraphics, point); subGraphics.dispose(); return dimension; } + private boolean shouldInvalidateOverlays() + { + final Client client = clientProvider.get(); + final Widget widget = client.getWidget(WidgetInfo.CHATBOX_MESSAGES); + final boolean resizeableChanged = isResizeable != client.isResized(); + + if (resizeableChanged) + { + isResizeable = client.isResized(); + return true; + } + + final boolean chatboxHiddenChanged = chatboxHidden != (widget != null && widget.isHidden()); + + if (chatboxHiddenChanged) + { + chatboxHidden = widget != null && widget.isHidden(); + return true; + } + + final boolean viewportChanged = !client.getViewportWidget().getBounds().equals(viewportBounds); + + if (viewportChanged) + { + viewportBounds = client.getViewportWidget().getBounds(); + return true; + } + + return false; + } + + private OverlayBounds buildSnapCorners() + { + final Client client = clientProvider.get(); + + final Rectangle bounds = viewportBounds != null + ? viewportBounds + : new Rectangle(0, 0, client.getCanvas().getWidth(), client.getCanvas().getHeight()); + + final Widget chatbox = client.getWidget(WidgetInfo.CHATBOX_MESSAGES); + final Rectangle chatboxBounds = chatbox != null + ? chatbox.getBounds() : new Rectangle(0, bounds.height, 519, 165); + + final Point topLeftPoint = new Point( + isResizeable ? BORDER_LEFT_RESIZABLE : BORDER_LEFT_FIXED, + isResizeable ? BORDER_TOP_RESIZABLE : BORDER_TOP_FIXED); + final Point topRightPoint = new Point(bounds.x + bounds.width - BORDER_RIGHT, BORDER_TOP_FIXED); + final Point bottomLeftPoint = new Point(isResizeable ? BORDER_LEFT_RESIZABLE : BORDER_LEFT_FIXED, bounds.y + bounds.height - BORDER_BOTTOM); + final Point bottomRightPoint = new Point(bounds.x + bounds.width - BORDER_RIGHT, bounds.y + bounds.height - BORDER_BOTTOM); + final Point rightChatboxPoint = new Point(bounds.x + chatboxBounds.width - BORDER_RIGHT, bounds.y + bounds.height - BORDER_BOTTOM); + + // Check to see if chat box is minimized + if (chatbox != null && isResizeable && chatboxHidden) + { + rightChatboxPoint.y += chatboxBounds.height; + bottomLeftPoint.y += chatboxBounds.height; + } + + return new OverlayBounds( + new Rectangle(topLeftPoint, SNAP_CORNER_SIZE), + new Rectangle(topRightPoint, SNAP_CORNER_SIZE), + new Rectangle(bottomLeftPoint, SNAP_CORNER_SIZE), + new Rectangle(bottomRightPoint, SNAP_CORNER_SIZE), + new Rectangle(rightChatboxPoint, SNAP_CORNER_SIZE)); + } + + private OverlayBounds translateSnapCorners(OverlayBounds snapCorners) + { + return new OverlayBounds( + snapCorners.getTopLeft(), + translate(snapCorners.getTopRight(), -SNAP_CORNER_SIZE.width, 0), + translate(snapCorners.getBottomLeft(), 0, -SNAP_CORNER_SIZE.height), + translate(snapCorners.getBottomRight(), -SNAP_CORNER_SIZE.width, -SNAP_CORNER_SIZE.height), + translate(snapCorners.getAboveChatboxRight(), -SNAP_CORNER_SIZE.width, -SNAP_CORNER_SIZE.height)); + } + + private static Rectangle translate(Rectangle rectangle, int dx, int dy) + { + rectangle.translate(dx, dy); + return rectangle; + } + + private void saveOverlayLocation(final Overlay overlay) + { + final String key = overlay.getClass().getSimpleName() + OVERLAY_CONFIG_PREFERRED_LOCATION; + if (overlay.getPreferredLocation() != null) + { + configManager.setConfiguration( + runeliteGroupName, + key, + overlay.getPreferredLocation()); + } + else + { + configManager.unsetConfiguration( + runeliteGroupName, + key); + } + } + + private void saveOverlayPosition(final Overlay overlay) + { + final String key = overlay.getClass().getSimpleName() + OVERLAY_CONFIG_PREFERRED_POSITION; + if (overlay.getPreferredPosition() != null) + { + configManager.setConfiguration( + runeliteGroupName, + key, + overlay.getPreferredPosition()); + } + else + { + configManager.unsetConfiguration( + runeliteGroupName, + key); + } + } + + private Point loadOverlayLocation(final Overlay overlay) + { + final String key = overlay.getClass().getSimpleName() + OVERLAY_CONFIG_PREFERRED_LOCATION; + return configManager.getConfiguration(runeliteGroupName, key, Point.class); + } + + private OverlayPosition loadOverlayPosition(final Overlay overlay) + { + final String locationKey = overlay.getClass().getSimpleName() + OVERLAY_CONFIG_PREFERRED_POSITION; + return configManager.getConfiguration(runeliteGroupName, locationKey, OverlayPosition.class); + } + public void provideScreenshot(BufferedImage image) { Consumer consumer; diff --git a/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayUtil.java b/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayUtil.java index 199b4c54d2..356a6aeed6 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayUtil.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/overlay/OverlayUtil.java @@ -137,6 +137,35 @@ public class OverlayUtil graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } + public static java.awt.Point padPosition(OverlayPosition position, Dimension dimension, final int padding) + { + final java.awt.Point result = new java.awt.Point(); + + switch (position) + { + case DYNAMIC: + case TOOLTIP: + break; + case BOTTOM_LEFT: + result.x += dimension.width + (dimension.width == 0 ? 0 : padding); + break; + case BOTTOM_RIGHT: + result.x -= dimension.width + (dimension.width == 0 ? 0 : padding); + break; + case TOP_LEFT: + result.y += dimension.height + (dimension.height == 0 ? 0 : padding); + break; + case TOP_RIGHT: + result.y += dimension.height + (dimension.height == 0 ? 0 : padding); + break; + case ABOVE_CHATBOX_RIGHT: + result.y -= dimension.height + (dimension.height == 0 ? 0 : padding); + break; + } + + return result; + } + public static java.awt.Point transformPosition(OverlayPosition position, Dimension dimension) { final java.awt.Point result = new java.awt.Point(); @@ -144,6 +173,7 @@ public class OverlayUtil switch (position) { case DYNAMIC: + case TOOLTIP: case TOP_LEFT: break; case BOTTOM_LEFT: