diff --git a/.classpath b/.classpath index fc2b6d2..49d75d3 100644 --- a/.classpath +++ b/.classpath @@ -12,5 +12,8 @@ + + + diff --git a/.gitignore b/.gitignore index 8f5d6dc..e11ffda 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ Thumbs.db /[Bb]uild/ /[Dd]ist/ /[Dd]oc/ +out/ # Ignore files created by posix people *~ # Ignore vim swapfiles diff --git a/assets/2001tray.png b/assets/2001tray.png index d611a12..e10f3ec 100644 Binary files a/assets/2001tray.png and b/assets/2001tray.png differ diff --git a/assets/gear.png b/assets/gear.png new file mode 100644 index 0000000..c174dc1 Binary files /dev/null and b/assets/gear.png differ diff --git a/assets/gear_gold.png b/assets/gear_gold.png new file mode 100644 index 0000000..a7c2c94 Binary files /dev/null and b/assets/gear_gold.png differ diff --git a/assets/icon-large.png b/assets/icon-large.png index d997faa..5df58a0 100644 Binary files a/assets/icon-large.png and b/assets/icon-large.png differ diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000..e94e6fb Binary files /dev/null and b/assets/icon.png differ diff --git a/assets/icon_warn.png b/assets/icon_warn.png new file mode 100644 index 0000000..1f03b50 Binary files /dev/null and b/assets/icon_warn.png differ diff --git a/build.xml b/build.xml index b7f358a..d65f456 100644 --- a/build.xml +++ b/build.xml @@ -103,6 +103,7 @@ + diff --git a/lib/flatlaf-3.1.1.jar b/lib/flatlaf-3.1.1.jar new file mode 100644 index 0000000..2b13cdd Binary files /dev/null and b/lib/flatlaf-3.1.1.jar differ diff --git a/lib/jna-5.13.0.jar b/lib/jna-5.13.0.jar new file mode 100644 index 0000000..3d49c81 Binary files /dev/null and b/lib/jna-5.13.0.jar differ diff --git a/lib/jna-platform-5.13.0.jar b/lib/jna-platform-5.13.0.jar new file mode 100644 index 0000000..816a567 Binary files /dev/null and b/lib/jna-platform-5.13.0.jar differ diff --git a/src/Client/ConfigWindow.java b/src/Client/ConfigWindow.java index a536785..ec2d0b6 100644 --- a/src/Client/ConfigWindow.java +++ b/src/Client/ConfigWindow.java @@ -18,17 +18,24 @@ */ package Client; +import static Client.Util.isDarkThemeFlatLAF; +import static Client.Util.isUsingFlatLAFTheme; +import static Client.Util.osScaleDiv; +import static Client.Util.osScaleMul; + import Client.KeybindSet.KeyModifier; import Game.Camera; import Game.Client; import Game.Game; import Game.KeyboardHandler; +import Game.Renderer; +import com.formdev.flatlaf.ui.FlatRoundBorder; +import java.awt.AWTEvent; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; -import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; @@ -38,14 +45,23 @@ import java.awt.Insets; import java.awt.RenderingHints; import java.awt.SystemTray; +import java.awt.Toolkit; +import java.awt.event.AWTEventListener; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.image.AffineTransformOp; import java.awt.image.BufferedImage; import java.lang.reflect.InvocationTargetException; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; @@ -59,6 +75,7 @@ import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JCheckBox; +import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; @@ -74,13 +91,11 @@ import javax.swing.SpinnerNumberModel; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; -import javax.swing.UIManager; -import javax.swing.UIManager.LookAndFeelInfo; -import javax.swing.UnsupportedLookAndFeelException; -import javax.swing.border.EmptyBorder; +import javax.swing.ToolTipManager; +import javax.swing.border.LineBorder; +import javax.swing.border.MatteBorder; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; -import javax.swing.plaf.nimbus.NimbusLookAndFeel; /** * GUI designed for the RSCPlus client that manages configuration options and keybind values from @@ -123,6 +138,15 @@ public class ConfigWindow { ButtonFocusListener focusListener = new ButtonFocusListener(); JTabbedPane tabbedPane; + // Tooltip-related components + private final AWTEventListener eventQueueListener; + private final String toolTipInitText = + "Click here to display additional information about settings"; + private boolean isListeningForEventQueue = false; + private JPanel toolTipPanel; + private JLabel toolTipTextLabel; + private String toolTipTextString; + /* * JComponent variables which hold configuration data */ @@ -131,6 +155,15 @@ public class ConfigWindow { private JCheckBox generalPanelClientSizeCheckbox; private JSpinner generalPanelClientSizeXSpinner; private JSpinner generalPanelClientSizeYSpinner; + private SpinnerNumberModel spinnerWinXModel; + private SpinnerNumberModel spinnerWinYModel; + private JCheckBox generalPanelScaleWindowCheckbox; + private JRadioButton generalPanelIntegerScalingFocusButton; + private JSpinner generalPanelIntegerScalingSpinner; + private JRadioButton generalPanelBilinearScalingFocusButton; + private JSpinner generalPanelBilinearScalingSpinner; + private JRadioButton generalPanelBicubicScalingFocusButton; + private JSpinner generalPanelBicubicScalingSpinner; private JCheckBox generalPanelCheckUpdates; private JCheckBox generalPanelWelcomeEnabled; // private JCheckBox generalPanelChatHistoryCheckbox; @@ -159,12 +192,16 @@ public class ConfigWindow { private JCheckBox generalPanelColoredTextCheckbox; private JSlider generalPanelFoVSlider; private JCheckBox generalPanelCustomCursorCheckbox; + private JCheckBox generalPanelShiftScrollCameraRotationCheckbox; + private JSlider generalPanelTrackpadRotationSlider; private JSlider generalPanelViewDistanceSlider; private JCheckBox generalPanelLimitFPSCheckbox; private JSpinner generalPanelLimitFPSSpinner; private JCheckBox generalPanelAutoScreenshotCheckbox; private JCheckBox generalPanelPatchGenderCheckbox; private JCheckBox generalPanelPatchHbar512LastPixelCheckbox; + private JCheckBox generalPanelUseDarkModeCheckbox; + private JCheckBox generalPanelUseNimbusThemeCheckbox; private JCheckBox generalPanelDebugModeCheckbox; private JCheckBox generalPanelExceptionHandlerCheckbox; private JLabel generalPanelNamePatchModeDesc; @@ -206,7 +243,6 @@ public class ConfigWindow { private JCheckBox notificationPanelPMNotifsCheckbox; private JCheckBox notificationPanelTradeNotifsCheckbox; private JCheckBox notificationPanelUnderAttackNotifsCheckbox; - private JCheckBox notificationPanelLogoutNotifsCheckbox; private JCheckBox notificationPanelLowHPNotifsCheckbox; private JSpinner notificationPanelLowHPNotifsSpinner; private JCheckBox notificationPanelHighlightedItemTimerCheckbox; @@ -250,26 +286,8 @@ public class ConfigWindow { private JPanel worldListPanel = new JPanel(); public ConfigWindow() { - try { - // Set System L&F as a fall-back option. - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) { - if ("Nimbus".equals(info.getName())) { - UIManager.setLookAndFeel(info.getClassName()); - NimbusLookAndFeel laf = (NimbusLookAndFeel) UIManager.getLookAndFeel(); - laf.getDefaults().put("defaultFont", new Font(Font.SANS_SERIF, Font.PLAIN, 11)); - break; - } - } - } catch (UnsupportedLookAndFeelException e) { - Logger.Error("Unable to set L&F: Unsupported look and feel"); - } catch (ClassNotFoundException e) { - Logger.Error("Unable to set L&F: Class not found"); - } catch (InstantiationException e) { - Logger.Error("Unable to set L&F: Class object cannot be instantiated"); - } catch (IllegalAccessException e) { - Logger.Error("Unable to set L&F: Illegal access exception"); - } + Util.setUITheme(); + eventQueueListener = createConfigWindowEventQueueListener(); initialize(); } @@ -279,9 +297,23 @@ public void showConfigWindow() { } public void hideConfigWindow() { + resetToolTipListener(); + frame.setVisible(false); } + public void toggleConfigWindow() { + if (this.isShown()) { + this.hideConfigWindow(); + } else { + this.showConfigWindow(); + } + } + + public boolean isShown() { + return frame.isVisible(); + } + /** Initialize the contents of the frame. */ private void initialize() { Logger.Info("Creating configuration window"); @@ -307,18 +339,47 @@ public void run() { private void runInit() { frame = new JFrame(); frame.setTitle("Settings"); - frame.setBounds(100, 100, 800, 650); + frame.setBounds(osScaleDiv(100), osScaleDiv(100), osScaleMul(800), osScaleMul(650)); frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); frame.getContentPane().setLayout(new BorderLayout(0, 0)); - URL iconURL = Launcher.getResource("/assets/RSCX.logo.png"); + URL iconURL = Launcher.getResource("/assets/icon.png"); if (iconURL != null) { ImageIcon icon = new ImageIcon(iconURL); frame.setIconImage(icon.getImage()); } + frame.addWindowListener( + new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + resetToolTipListener(); + super.windowClosed(e); + } + }); // Container declarations /** The tabbed pane holding the five configuration tabs */ tabbedPane = new JTabbedPane(); + if (isUsingFlatLAFTheme()) { + tabbedPane.putClientProperty("JTabbedPane.tabType", "card"); + } + tabbedPane.addMouseListener( + new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + super.mouseClicked(e); + if (isListeningForEventQueue) { + toolTipTextString = "Waiting for mouse hover..."; + } else { + toolTipTextString = toolTipInitText; + } + toolTipTextLabel.setText(toolTipTextString); + } + }); + + /* The JPanel containing the tooltip text components */ + toolTipPanel = new JPanel(); + resetToolTipBarPanelColors(); + /** * The JPanel containing the OK, Cancel, Apply, and Restore Defaults buttons at the bottom of * the window @@ -334,22 +395,68 @@ private void runInit() { JScrollPane worldListScrollPane = new JScrollPane(); JScrollPane authorsScrollPane = new JScrollPane(); + if (isUsingFlatLAFTheme()) { + Color navigationPanelBackgroundColor = null; + + if (isDarkThemeFlatLAF()) { + navigationPanelBackgroundColor = new Color(60, 63, 65); + } else if (Util.isLightThemeFlatLAF()) { + navigationPanelBackgroundColor = new Color(225, 225, 225); + } + + navigationPanel.setBackground(navigationPanelBackgroundColor); + + Color scrollPaneBorderColor = null; + + if (isDarkThemeFlatLAF()) { + scrollPaneBorderColor = new Color(82, 86, 87); + } else if (Util.isLightThemeFlatLAF()) { + scrollPaneBorderColor = new Color(194, 194, 194); + } + + MatteBorder scrollPaneBorder = + BorderFactory.createMatteBorder( + 0, osScaleMul(1), osScaleMul(1), osScaleMul(1), scrollPaneBorderColor); + + presetsScrollPane.setBorder(scrollPaneBorder); + generalScrollPane.setBorder(scrollPaneBorder); + overlayScrollPane.setBorder(scrollPaneBorder); + notificationScrollPane.setBorder(scrollPaneBorder); + streamingScrollPane.setBorder(scrollPaneBorder); + keybindScrollPane.setBorder(scrollPaneBorder); + worldListScrollPane.setBorder(scrollPaneBorder); + authorsScrollPane.setBorder(scrollPaneBorder); + } + JPanel presetsPanel = new JPanel(); + presetsPanel.setName("presets"); JPanel generalPanel = new JPanel(); + generalPanel.setName("general"); JPanel overlayPanel = new JPanel(); + overlayPanel.setName("overlays"); JPanel notificationPanel = new JPanel(); + notificationPanel.setName("notifications"); JPanel streamingPanel = new JPanel(); + streamingPanel.setName("streaming_privacy"); JPanel keybindPanel = new JPanel(); + keybindPanel.setName("keybinds"); worldListPanel = new JPanel(); + worldListPanel.setName("world_list"); JPanel authorsPanel = new JPanel(); + authorsPanel.setName("authors"); frame.getContentPane().add(tabbedPane, BorderLayout.CENTER); - frame.getContentPane().add(navigationPanel, BorderLayout.PAGE_END); + + JPanel pageEndPanel = new JPanel(); + pageEndPanel.setLayout(new BoxLayout(pageEndPanel, BoxLayout.Y_AXIS)); + pageEndPanel.add(toolTipPanel); + pageEndPanel.add(navigationPanel); + frame.getContentPane().add(pageEndPanel, BorderLayout.PAGE_END); tabbedPane.addTab("Presets", null, presetsScrollPane, null); tabbedPane.addTab("General", null, generalScrollPane, null); tabbedPane.addTab("Overlays", null, overlayScrollPane, null); - // tabbedPane.addTab("Notifications", null, notificationScrollPane, null); + tabbedPane.addTab("Notifications", null, notificationScrollPane, null); tabbedPane.addTab("Streaming & Privacy", null, streamingScrollPane, null); tabbedPane.addTab("Keybinds", null, keybindScrollPane, null); tabbedPane.addTab("World List", null, worldListScrollPane, null); @@ -365,24 +472,80 @@ private void runInit() { authorsScrollPane.setViewportView(authorsPanel); // Adding padding for aesthetics - navigationPanel.setBorder(BorderFactory.createEmptyBorder(7, 10, 10, 10)); - presetsPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - generalPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - overlayPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - notificationPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - streamingPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - keybindPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - worldListPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - authorsPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - - setScrollSpeed(presetsScrollPane, 20, 15); - setScrollSpeed(generalScrollPane, 20, 15); - setScrollSpeed(overlayScrollPane, 20, 15); - setScrollSpeed(notificationScrollPane, 20, 15); - setScrollSpeed(streamingScrollPane, 20, 15); - setScrollSpeed(keybindScrollPane, 20, 15); - setScrollSpeed(worldListScrollPane, 20, 15); - setScrollSpeed(authorsScrollPane, 20, 15); + int border10 = osScaleMul(10); + if (Util.isUsingFlatLAFTheme()) { + Color borderColor = isDarkThemeFlatLAF() ? new Color(82, 86, 87) : new Color(194, 194, 194); + toolTipPanel.setBorder( + BorderFactory.createMatteBorder( + 0, osScaleMul(1), osScaleMul(1), osScaleMul(1), borderColor)); + } else { + toolTipPanel.setBorder( + BorderFactory.createCompoundBorder( + BorderFactory.createEmptyBorder(0, osScaleMul(2), 0, osScaleMul(2)), + BorderFactory.createLineBorder(new Color(146, 151, 161)))); + } + navigationPanel.setBorder( + BorderFactory.createEmptyBorder(osScaleMul(7), border10, border10, border10)); + presetsPanel.setBorder(BorderFactory.createEmptyBorder(border10, border10, border10, border10)); + generalPanel.setBorder(BorderFactory.createEmptyBorder(border10, border10, border10, border10)); + overlayPanel.setBorder(BorderFactory.createEmptyBorder(border10, border10, border10, border10)); + notificationPanel.setBorder( + BorderFactory.createEmptyBorder(border10, border10, border10, border10)); + streamingPanel.setBorder( + BorderFactory.createEmptyBorder(border10, border10, border10, border10)); + keybindPanel.setBorder(BorderFactory.createEmptyBorder(border10, border10, border10, border10)); + worldListPanel.setBorder( + BorderFactory.createEmptyBorder(border10, border10, border10, border10)); + authorsPanel.setBorder(BorderFactory.createEmptyBorder(border10, border10, border10, border10)); + + int verticalSpeed = osScaleMul(20); + int horizontalSpeed = osScaleMul(15); + + setScrollSpeed(presetsScrollPane, verticalSpeed, horizontalSpeed); + setScrollSpeed(generalScrollPane, verticalSpeed, horizontalSpeed); + setScrollSpeed(overlayScrollPane, verticalSpeed, horizontalSpeed); + setScrollSpeed(notificationScrollPane, verticalSpeed, horizontalSpeed); + setScrollSpeed(streamingScrollPane, verticalSpeed, horizontalSpeed); + setScrollSpeed(keybindScrollPane, verticalSpeed, horizontalSpeed); + setScrollSpeed(worldListScrollPane, verticalSpeed, horizontalSpeed); + setScrollSpeed(authorsScrollPane, verticalSpeed, horizontalSpeed); + + /* + Tooltip panel + */ + toolTipPanel.setLayout(new BoxLayout(toolTipPanel, BoxLayout.X_AXIS)); + toolTipTextLabel = new JLabel(toolTipInitText); + toolTipTextLabel.setBorder( + BorderFactory.createEmptyBorder(osScaleMul(2), border10, osScaleMul(2), border10)); + toolTipTextLabel.setText(toolTipInitText); + toolTipTextLabel.setMinimumSize(osScaleMul(new Dimension(100, 28))); + toolTipTextLabel.setMaximumSize(new Dimension(Short.MAX_VALUE, osScaleMul(28))); + toolTipTextLabel.setAlignmentY(0.75f); + toolTipPanel.add(toolTipTextLabel); + + toolTipPanel.addMouseListener( + new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + if (isListeningForEventQueue) { + resetToolTipBarPanelColors(); + toolTipTextLabel.setText(toolTipInitText); + removeConfigWindowEventQueueListener(); + } else { + // Uses theme tooltip colors + if (Util.isDarkThemeFlatLAF()) { + toolTipPanel.setBackground(new Color(21, 23, 24)); + } else if (Util.isLightThemeFlatLAF()) { + toolTipPanel.setBackground(new Color(250, 250, 250)); + } else { + toolTipPanel.setBackground(new Color(242, 242, 189)); + } + toolTipTextString = "Waiting for mouse hover..."; + toolTipTextLabel.setText(toolTipTextString); + addConfigWindowEventQueueListener(); + } + } + }); /* * Navigation buttons @@ -390,86 +553,116 @@ private void runInit() { navigationPanel.setLayout(new BoxLayout(navigationPanel, BoxLayout.X_AXIS)); - addButton("OK", navigationPanel, Component.LEFT_ALIGNMENT) - .addActionListener( - new ActionListener() { + JButton okButton = addButton("OK", navigationPanel, Component.LEFT_ALIGNMENT); + if (isDarkThemeFlatLAF()) { + okButton.setBackground(new Color(42, 46, 48)); + } + okButton.addActionListener( + new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - Launcher.getConfigWindow().saveSettings(); - Launcher.getConfigWindow().hideConfigWindow(); - } - }); + @Override + public void actionPerformed(ActionEvent e) { + Launcher.getConfigWindow().applySettings(); + Launcher.getConfigWindow().hideConfigWindow(); + } + }); - addButton("Cancel", navigationPanel, Component.LEFT_ALIGNMENT) - .addActionListener( - new ActionListener() { + if (isUsingFlatLAFTheme()) { + navigationPanel.add(Box.createRigidArea(osScaleMul(new Dimension(4, 0)))); + } - @Override - public void actionPerformed(ActionEvent e) { - Launcher.getConfigWindow().applySettings(); - Launcher.getConfigWindow().hideConfigWindow(); - } - }); + JButton cancelButton = addButton("Cancel", navigationPanel, Component.LEFT_ALIGNMENT); - addButton("Apply", navigationPanel, Component.LEFT_ALIGNMENT) - .addActionListener( - new ActionListener() { + if (isDarkThemeFlatLAF()) { + cancelButton.setBackground(new Color(42, 46, 48)); + } - @Override - public void actionPerformed(ActionEvent e) { - Launcher.getConfigWindow().applySettings(); - } - }); + cancelButton.addActionListener( + new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + Launcher.getConfigWindow().hideConfigWindow(); + } + }); + + if (isUsingFlatLAFTheme()) { + navigationPanel.add(Box.createRigidArea(osScaleMul(new Dimension(4, 0)))); + } + + JButton applyButton = addButton("Apply", navigationPanel, Component.LEFT_ALIGNMENT); + + if (isDarkThemeFlatLAF()) { + applyButton.setBackground(new Color(42, 46, 48)); + } + + applyButton.addActionListener( + new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + Launcher.getConfigWindow().applySettings(); + } + }); navigationPanel.add(Box.createHorizontalGlue()); - addButton("Restore Defaults", navigationPanel, Component.RIGHT_ALIGNMENT) - .addActionListener( - new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - int choice = - JOptionPane.showConfirmDialog( - Launcher.getConfigWindow().frame, - "Are you sure you want to restore all settings to their defaults?", - "Confirm", - JOptionPane.YES_NO_OPTION, - JOptionPane.QUESTION_MESSAGE); - if (choice == JOptionPane.CLOSED_OPTION || choice == JOptionPane.NO_OPTION) { - return; - } + JButton restoreDefaultsButton = + addButton("Restore Defaults", navigationPanel, Component.RIGHT_ALIGNMENT); - Settings.initSettings(); // make sure "default" is really default - Settings.save("default"); - synchronizeGuiValues(); - - // Restore defaults - /* TODO: reimplement per-tab defaults? - switch (tabbedPane.getSelectedIndex()) { - case 0: - Settings.restoreDefaultGeneral(); - Game.getInstance().resizeFrameWithContents(); - break; - case 1: - Settings.restoreDefaultOverlays(); - break; - case 2: - Settings.restoreDefaultNotifications(); - break; - case 3: - Settings.restoreDefaultPrivacy(); - break; - case 4: - Settings.restoreDefaultKeybinds(); - break; - //TODO more pages - default: - Logger.Error("Restore defaults attempted to operate on a non-existent tab!"); - } - */ - } - }); + if (isDarkThemeFlatLAF()) { + restoreDefaultsButton.setBackground(new Color(42, 46, 48)); + } + + restoreDefaultsButton.addActionListener( + new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + JPanel confirmDefaultPanel = + Util.createOptionMessagePanel( + "Are you sure you want to restore all settings to their defaults?"); + int choice = + JOptionPane.showConfirmDialog( + Launcher.getConfigWindow().frame, + confirmDefaultPanel, + "Confirm", + JOptionPane.YES_NO_OPTION, + JOptionPane.QUESTION_MESSAGE); + if (choice == JOptionPane.CLOSED_OPTION || choice == JOptionPane.NO_OPTION) { + return; + } + + Settings.initSettings(); // make sure "default" is really default + Settings.save("default"); + synchronizeGuiValues(); + + // Restore defaults + /* TODO: reimplement per-tab defaults? + switch (tabbedPane.getSelectedIndex()) { + case 0: + Settings.restoreDefaultGeneral(); + Game.getInstance().resizeFrameWithContents(); + break; + case 1: + Settings.restoreDefaultOverlays(); + break; + case 2: + Settings.restoreDefaultNotifications(); + break; + case 3: + Settings.restoreDefaultPrivacy(); + break; + case 4: + Settings.restoreDefaultKeybinds(); + break; + //TODO more pages + default: + Logger.Error("Restore defaults attempted to operate on a non-existent tab!"); + } + */ + } + }); /* * General tab @@ -486,193 +679,388 @@ public void actionPerformed(ActionEvent e) { generalPanel.add(generalPanelClientSizePanel); generalPanelClientSizePanel.setLayout( new BoxLayout(generalPanelClientSizePanel, BoxLayout.X_AXIS)); - generalPanelClientSizePanel.setPreferredSize(new Dimension(0, 37)); generalPanelClientSizePanel.setAlignmentX(Component.LEFT_ALIGNMENT); // TODO: Perhaps change to "Save client size on close"? generalPanelClientSizeCheckbox = - addCheckbox("Default client size:", generalPanelClientSizePanel); - generalPanelClientSizeCheckbox.setToolTipText("Start the client with the supplied window size"); + addCheckbox("Client window dimensions:", generalPanelClientSizePanel); + generalPanelClientSizeCheckbox.setToolTipText("Set the client size to the supplied dimensions"); generalPanelClientSizeXSpinner = new JSpinner(); generalPanelClientSizePanel.add(generalPanelClientSizeXSpinner); - generalPanelClientSizeXSpinner.setMaximumSize(new Dimension(58, 22)); - generalPanelClientSizeXSpinner.setMinimumSize(new Dimension(58, 22)); - generalPanelClientSizeXSpinner.setAlignmentY((float) 0.75); - generalPanelClientSizeXSpinner.setToolTipText("Default client width (512 minimum)"); + generalPanelClientSizeXSpinner.setMaximumSize(osScaleMul(new Dimension(70, 23))); + generalPanelClientSizeXSpinner.setMinimumSize(osScaleMul(new Dimension(70, 23))); + generalPanelClientSizeXSpinner.setAlignmentY(0.7f); + generalPanelClientSizeXSpinner.setToolTipText("Default client width (512 minimum at 1x scale)"); generalPanelClientSizeXSpinner.putClientProperty("JComponent.sizeVariant", "mini"); JLabel generalPanelClientSizeByLabel = new JLabel("x"); generalPanelClientSizePanel.add(generalPanelClientSizeByLabel); - generalPanelClientSizeByLabel.setAlignmentY((float) 0.9); - generalPanelClientSizeByLabel.setBorder(new EmptyBorder(0, 2, 0, 2)); + generalPanelClientSizeByLabel.setAlignmentY(0.8f); + + int spinnerByMargin = isUsingFlatLAFTheme() ? 4 : 2; + + generalPanelClientSizeByLabel.setBorder( + BorderFactory.createEmptyBorder( + 0, osScaleMul(spinnerByMargin), 0, osScaleMul(spinnerByMargin))); generalPanelClientSizeYSpinner = new JSpinner(); generalPanelClientSizePanel.add(generalPanelClientSizeYSpinner); - generalPanelClientSizeYSpinner.setMaximumSize(new Dimension(58, 22)); - generalPanelClientSizeYSpinner.setMinimumSize(new Dimension(58, 22)); - generalPanelClientSizeYSpinner.setAlignmentY((float) 0.75); - generalPanelClientSizeYSpinner.setToolTipText("Default client height (357 minimum)"); + generalPanelClientSizeYSpinner.setMaximumSize(osScaleMul(new Dimension(70, 23))); + generalPanelClientSizeYSpinner.setMinimumSize(osScaleMul(new Dimension(70, 23))); + generalPanelClientSizeYSpinner.setAlignmentY(0.7f); + generalPanelClientSizeYSpinner.setToolTipText( + "Default client height (357 minimum at 1x scale)"); generalPanelClientSizeYSpinner.putClientProperty("JComponent.sizeVariant", "mini"); // Sanitize JSpinner values - SpinnerNumberModel spinnerWinXModel = new SpinnerNumberModel(); + spinnerWinXModel = new SpinnerNumberModel(); spinnerWinXModel.setMinimum(512); spinnerWinXModel.setValue(512); spinnerWinXModel.setStepSize(10); generalPanelClientSizeXSpinner.setModel(spinnerWinXModel); - SpinnerNumberModel spinnerWinYModel = new SpinnerNumberModel(); + spinnerWinYModel = new SpinnerNumberModel(); spinnerWinYModel.setMinimum(357); spinnerWinYModel.setValue(357); spinnerWinYModel.setStepSize(10); generalPanelClientSizeYSpinner.setModel(spinnerWinYModel); - generalPanelCheckUpdates = - addCheckbox("Check for rsctimes updates from GitHub at launch", generalPanel); - generalPanelCheckUpdates.setToolTipText( - "When enabled, rsctimes will check for client updates before launching the game and install them when prompted"); - - generalPanelWelcomeEnabled = - addCheckbox("Remind you how to open the Settings every time you log in", generalPanel); - generalPanelWelcomeEnabled.setToolTipText( - "When enabled, rsctimes will insert a message telling the current keybinding to open the settings menu and remind you about the tray icon"); + if (Util.isUsingFlatLAFTheme()) { + generalPanelClientSizePanel.add(Box.createRigidArea(osScaleMul(new Dimension(6, 0)))); + } - generalPanelCustomCursorCheckbox = addCheckbox("Use custom mouse cursor", generalPanel); - generalPanelCustomCursorCheckbox.setToolTipText( - "Switch to using a custom mouse cursor instead of the system default"); + JButton generalPanelClientSizeMaxButton = + addButton("Max", generalPanelClientSizePanel, Component.RIGHT_ALIGNMENT); + generalPanelClientSizeMaxButton.setAlignmentY(0.7f); + generalPanelClientSizeMaxButton.addActionListener( + new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + Dimension maximumWindowSize = + ScaledWindow.getInstance().getMaximumEffectiveWindowSize(); - generalPanelAutoScreenshotCheckbox = - addCheckbox("Take a screenshot when you level up or complete a quest", generalPanel); - generalPanelAutoScreenshotCheckbox.setToolTipText( - "Takes a screenshot for you for level ups and quest completion"); + int windowWidth = maximumWindowSize.width; + int windowHeight = maximumWindowSize.height; - JLabel generalPanelFoVLabel = new JLabel("Field of view (Default 9)"); - generalPanelFoVLabel.setToolTipText("Sets the field of view (not recommended past 10)"); - generalPanel.add(generalPanelFoVLabel); - generalPanelFoVLabel.setAlignmentY((float) 0.9); + // This only changes the values in the boxes + spinnerWinXModel.setValue(windowWidth); + spinnerWinYModel.setValue(windowHeight); + } + }); - generalPanelFoVSlider = new JSlider(); + if (Util.isUsingFlatLAFTheme()) { + generalPanelClientSizePanel.add(Box.createRigidArea(osScaleMul(new Dimension(6, 0)))); + } - generalPanel.add(generalPanelFoVSlider); - generalPanelFoVSlider.setAlignmentX(Component.LEFT_ALIGNMENT); - generalPanelFoVSlider.setMaximumSize(new Dimension(200, 55)); - generalPanelFoVSlider.setBorder(new EmptyBorder(0, 0, 5, 0)); - generalPanelFoVSlider.setMinimum(7); - generalPanelFoVSlider.setMaximum(16); - generalPanelFoVSlider.setMajorTickSpacing(1); - generalPanelFoVSlider.setPaintTicks(true); - generalPanelFoVSlider.setPaintLabels(true); + JButton generalPanelClientSizeResetButton = + addButton("Reset", generalPanelClientSizePanel, Component.RIGHT_ALIGNMENT); + generalPanelClientSizeResetButton.setAlignmentY(0.7f); + generalPanelClientSizeResetButton.addActionListener( + new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + // This only changes the values in the boxes + Dimension scaledMinimumWindowSize = + ScaledWindow.getInstance().getMinimumViewportSizeForScalar(); + spinnerWinXModel.setValue(scaledMinimumWindowSize.width); + spinnerWinYModel.setValue(scaledMinimumWindowSize.height); + } + }); - JLabel generalPanelViewDistanceLabel = new JLabel("View distance"); - generalPanelViewDistanceLabel.setToolTipText( - "Sets the max render distance of structures and landscape"); - generalPanel.add(generalPanelViewDistanceLabel); - generalPanelViewDistanceLabel.setAlignmentY((float) 0.9); + JLabel generalPanelClientSizeScaleWarning = + new JLabel("(Will be reset if window scale changes)"); + generalPanelClientSizeScaleWarning.setAlignmentY(0.8f); + generalPanelClientSizeScaleWarning.setBorder( + BorderFactory.createEmptyBorder(0, osScaleMul(6), 0, 0)); + generalPanelClientSizePanel.add(generalPanelClientSizeScaleWarning); + + // Scaling options + JPanel generalPanelScalePanel = new JPanel(); + generalPanel.add(generalPanelScalePanel); + generalPanelScalePanel.setLayout(new BoxLayout(generalPanelScalePanel, BoxLayout.Y_AXIS)); + generalPanelScalePanel.setAlignmentX(Component.LEFT_ALIGNMENT); + + generalPanelScaleWindowCheckbox = addCheckbox("Scale window:", generalPanelScalePanel); + generalPanelScaleWindowCheckbox.setToolTipText("Enable to scale the game client"); + generalPanelScaleWindowCheckbox.setBorder( + BorderFactory.createEmptyBorder(0, 0, osScaleMul(5), 0)); + + ButtonGroup generalPanelScaleWindowTypeButtonGroup = new ButtonGroup(); + String scaleLargerThanResolutionToolTip = + "This scale value will produce a window bigger than your screen resolution"; + + // Integer scaling + JPanel generalPanelIntegerScalingPanel = new JPanel(); + generalPanelScalePanel.add(generalPanelIntegerScalingPanel); + generalPanelIntegerScalingPanel.setLayout( + new BoxLayout(generalPanelIntegerScalingPanel, BoxLayout.X_AXIS)); + generalPanelIntegerScalingPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + + generalPanelIntegerScalingFocusButton = + addRadioButton("Integer scaling", generalPanelIntegerScalingPanel, osScaleMul(20)); + generalPanelIntegerScalingFocusButton.setToolTipText( + "Uses the nearest neighbor algorithm for pixel-perfect client scaling"); + generalPanelScaleWindowTypeButtonGroup.add(generalPanelIntegerScalingFocusButton); + + generalPanelIntegerScalingSpinner = new JSpinner(); + generalPanelIntegerScalingPanel.add(generalPanelIntegerScalingSpinner); + String integerScalingSpinnerToolTip = + "Integer scaling value " + (int) Renderer.minScalar + "-" + (int) Renderer.maxIntegerScalar; + generalPanelIntegerScalingSpinner.setMaximumSize(osScaleMul(new Dimension(55, 26))); + generalPanelIntegerScalingSpinner.setMinimumSize(osScaleMul(new Dimension(55, 26))); + generalPanelIntegerScalingSpinner.setAlignmentY(0.625f); + generalPanelIntegerScalingSpinner.setToolTipText(integerScalingSpinnerToolTip); + generalPanelIntegerScalingSpinner.putClientProperty("JComponent.sizeVariant", "mini"); + if (Util.isUsingFlatLAFTheme()) { + generalPanelIntegerScalingSpinner.setBorder(new FlatRoundBorder()); + } else { + generalPanelIntegerScalingSpinner.setBorder( + BorderFactory.createEmptyBorder( + osScaleMul(2), osScaleMul(2), osScaleMul(2), osScaleMul(2))); + } + generalPanelIntegerScalingSpinner.addChangeListener( + new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + Dimension maximumWindowSize = ScaledWindow.getInstance().getMaximumWindowSize(); + int scalar = (int) generalPanelIntegerScalingSpinner.getValue(); + + if (((512 * scalar) + ScaledWindow.getInstance().getWindowWidthInsets() + > maximumWindowSize.getWidth()) + || ((357 * scalar) + ScaledWindow.getInstance().getWindowHeightInsets() + > maximumWindowSize.getHeight())) { + generalPanelIntegerScalingSpinner.setBorder( + new LineBorder(Color.orange, osScaleMul(2))); + generalPanelIntegerScalingSpinner.setToolTipText(scaleLargerThanResolutionToolTip); + } else { + if (Util.isUsingFlatLAFTheme()) { + generalPanelIntegerScalingSpinner.setBorder(new FlatRoundBorder()); + } else { + generalPanelIntegerScalingSpinner.setBorder( + BorderFactory.createEmptyBorder( + osScaleMul(2), osScaleMul(2), osScaleMul(2), osScaleMul(2))); + } + generalPanelIntegerScalingSpinner.setToolTipText(integerScalingSpinnerToolTip); + } + } + }); - generalPanelViewDistanceSlider = new JSlider(); + SpinnerNumberModel spinnerLimitIntegerScaling = + new SpinnerNumberModel(2, (int) Renderer.minScalar, (int) Renderer.maxIntegerScalar, 1); + generalPanelIntegerScalingSpinner.setModel(spinnerLimitIntegerScaling); + + // Bilinear scaling + JPanel generalPanelBilinearScalingPanel = new JPanel(); + generalPanelScalePanel.add(generalPanelBilinearScalingPanel); + generalPanelBilinearScalingPanel.setLayout( + new BoxLayout(generalPanelBilinearScalingPanel, BoxLayout.X_AXIS)); + generalPanelBilinearScalingPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + + generalPanelBilinearScalingFocusButton = + addRadioButton("Bilinear interpolation", generalPanelBilinearScalingPanel, osScaleMul(20)); + generalPanelBilinearScalingFocusButton.setToolTipText( + "Uses the bilinear interpolation algorithm for client scaling"); + generalPanelScaleWindowTypeButtonGroup.add(generalPanelBilinearScalingFocusButton); + + generalPanelBilinearScalingSpinner = new JSpinner(); + generalPanelBilinearScalingPanel.add(generalPanelBilinearScalingSpinner); + String bilinearScalingSpinnerToolTip = + "Bilinear scaling value " + Renderer.minScalar + "-" + Renderer.maxInterpolationScalar; + generalPanelBilinearScalingSpinner.setMaximumSize(osScaleMul(new Dimension(55, 26))); + generalPanelBilinearScalingSpinner.setMinimumSize(osScaleMul(new Dimension(55, 26))); + generalPanelBilinearScalingSpinner.setAlignmentY(0.625f); + generalPanelBilinearScalingSpinner.setToolTipText(bilinearScalingSpinnerToolTip); + generalPanelBilinearScalingSpinner.putClientProperty("JComponent.sizeVariant", "mini"); + if (Util.isUsingFlatLAFTheme()) { + generalPanelBilinearScalingSpinner.setBorder(new FlatRoundBorder()); + } else { + generalPanelBilinearScalingSpinner.setBorder( + BorderFactory.createEmptyBorder( + osScaleMul(2), osScaleMul(2), osScaleMul(2), osScaleMul(2))); + } + generalPanelBilinearScalingSpinner.addChangeListener( + new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + Dimension maximumWindowSize = ScaledWindow.getInstance().getMaximumWindowSize(); + float scalar = (float) generalPanelBilinearScalingSpinner.getValue(); + + if (((512 * scalar) + ScaledWindow.getInstance().getWindowWidthInsets() + > maximumWindowSize.getWidth()) + || ((357 * scalar) + ScaledWindow.getInstance().getWindowHeightInsets() + > maximumWindowSize.getHeight())) { + generalPanelBilinearScalingSpinner.setBorder( + new LineBorder(Color.orange, osScaleMul(2))); + generalPanelBilinearScalingSpinner.setToolTipText(scaleLargerThanResolutionToolTip); + } else { + if (Util.isUsingFlatLAFTheme()) { + generalPanelBilinearScalingSpinner.setBorder(new FlatRoundBorder()); + } else { + generalPanelBilinearScalingSpinner.setBorder( + BorderFactory.createEmptyBorder( + osScaleMul(2), osScaleMul(2), osScaleMul(2), osScaleMul(2))); + } + generalPanelBilinearScalingSpinner.setToolTipText(bilinearScalingSpinnerToolTip); + } + } + }); - generalPanel.add(generalPanelViewDistanceSlider); - generalPanelViewDistanceSlider.setAlignmentX(Component.LEFT_ALIGNMENT); - generalPanelViewDistanceSlider.setMaximumSize(new Dimension(200, 55)); - generalPanelViewDistanceSlider.setBorder(new EmptyBorder(0, 0, 5, 0)); - generalPanelViewDistanceSlider.setMinorTickSpacing(500); - generalPanelViewDistanceSlider.setMajorTickSpacing(1000); - generalPanelViewDistanceSlider.setMinimum(2300); - generalPanelViewDistanceSlider.setMaximum(20000); - generalPanelViewDistanceSlider.setPaintTicks(true); + SpinnerNumberModel spinnerLimitBilinearScaling = + new SpinnerNumberModel( + new Float(1.5f), + new Float(Renderer.minScalar), + new Float(Renderer.maxInterpolationScalar), + new Float(0.1f)); + generalPanelBilinearScalingSpinner.setModel(spinnerLimitBilinearScaling); + + JLabel bilinearInterpolationScalingWarning = + new JLabel("(May affect performance at high scaling values)"); + bilinearInterpolationScalingWarning.setAlignmentY(0.75f); + int bilinearInterpolationScalingMargin = isUsingFlatLAFTheme() ? 6 : 2; + bilinearInterpolationScalingWarning.setBorder( + BorderFactory.createEmptyBorder(0, osScaleMul(bilinearInterpolationScalingMargin), 0, 0)); + generalPanelBilinearScalingPanel.add(bilinearInterpolationScalingWarning); + + // Bicubic scaling + JPanel generalPanelBicubicScalingPanel = new JPanel(); + generalPanelScalePanel.add(generalPanelBicubicScalingPanel); + generalPanelBicubicScalingPanel.setLayout( + new BoxLayout(generalPanelBicubicScalingPanel, BoxLayout.X_AXIS)); + generalPanelBicubicScalingPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + + generalPanelBicubicScalingFocusButton = + addRadioButton("Bicubic interpolation", generalPanelBicubicScalingPanel, osScaleMul(20)); + generalPanelBicubicScalingFocusButton.setToolTipText( + "Uses the bicubic interpolation algorithm for client scaling"); + generalPanelScaleWindowTypeButtonGroup.add(generalPanelBicubicScalingFocusButton); + + generalPanelBicubicScalingSpinner = new JSpinner(); + generalPanelBicubicScalingPanel.add(generalPanelBicubicScalingSpinner); + String bicubicScalingSpinnerToolTip = + "Bicubic scaling value " + Renderer.minScalar + "-" + Renderer.maxInterpolationScalar; + generalPanelBicubicScalingSpinner.setMaximumSize(osScaleMul(new Dimension(55, 26))); + generalPanelBicubicScalingSpinner.setMinimumSize(osScaleMul(new Dimension(55, 26))); + generalPanelBicubicScalingSpinner.setAlignmentY(0.625f); + generalPanelBicubicScalingSpinner.setToolTipText(bicubicScalingSpinnerToolTip); + generalPanelBicubicScalingSpinner.putClientProperty("JComponent.sizeVariant", "mini"); + if (Util.isUsingFlatLAFTheme()) { + generalPanelBicubicScalingSpinner.setBorder(new FlatRoundBorder()); + } else { + generalPanelBicubicScalingSpinner.setBorder( + BorderFactory.createEmptyBorder( + osScaleMul(2), osScaleMul(2), osScaleMul(2), osScaleMul(2))); + } + generalPanelBicubicScalingSpinner.addChangeListener( + new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + Dimension maximumWindowSize = ScaledWindow.getInstance().getMaximumWindowSize(); + float scalar = (float) generalPanelBicubicScalingSpinner.getValue(); + + if (((512 * scalar) + ScaledWindow.getInstance().getWindowWidthInsets() + > maximumWindowSize.getWidth()) + || ((357 * scalar) + ScaledWindow.getInstance().getWindowHeightInsets() + > maximumWindowSize.getHeight())) { + generalPanelBicubicScalingSpinner.setBorder(new LineBorder(Color.orange, 2)); + generalPanelBicubicScalingSpinner.setToolTipText(scaleLargerThanResolutionToolTip); + } else { + if (Util.isUsingFlatLAFTheme()) { + generalPanelBicubicScalingSpinner.setBorder(new FlatRoundBorder()); + } else { + generalPanelBicubicScalingSpinner.setBorder( + BorderFactory.createEmptyBorder( + osScaleMul(2), osScaleMul(2), osScaleMul(2), osScaleMul(2))); + } + generalPanelBicubicScalingSpinner.setToolTipText(bicubicScalingSpinnerToolTip); + } + } + }); - Hashtable generalPanelViewDistanceLabelTable = - new Hashtable(); - generalPanelViewDistanceLabelTable.put(new Integer(2300), new JLabel("2,300")); - generalPanelViewDistanceLabelTable.put(new Integer(10000), new JLabel("10,000")); - generalPanelViewDistanceLabelTable.put(new Integer(20000), new JLabel("20,000")); - generalPanelViewDistanceSlider.setLabelTable(generalPanelViewDistanceLabelTable); - generalPanelViewDistanceSlider.setPaintLabels(true); + SpinnerNumberModel spinnerLimitBicubicScaling = + new SpinnerNumberModel( + new Float(1.5f), + new Float(Renderer.minScalar), + new Float(Renderer.maxInterpolationScalar), + new Float(0.1f)); + generalPanelBicubicScalingSpinner.setModel(spinnerLimitBicubicScaling); + + JLabel bicubicInterpolationScalingWarning = + new JLabel("(May affect performance at high scaling values)"); + bicubicInterpolationScalingWarning.setAlignmentY(0.75f); + int bicubicInterpolationScalingMargin = isUsingFlatLAFTheme() ? 6 : 2; + bicubicInterpolationScalingWarning.setBorder( + BorderFactory.createEmptyBorder(0, osScaleMul(bicubicInterpolationScalingMargin), 0, 0)); + generalPanelBicubicScalingPanel.add(bicubicInterpolationScalingWarning); + + if (isUsingFlatLAFTheme()) { + generalPanelScalePanel.add(Box.createRigidArea(osScaleMul(new Dimension(0, 5)))); + } + // End scaling options - ////// - JPanel generalPanelLimitFPSPanel = new JPanel(); - generalPanel.add(generalPanelLimitFPSPanel); - generalPanelLimitFPSPanel.setLayout(new BoxLayout(generalPanelLimitFPSPanel, BoxLayout.X_AXIS)); - generalPanelLimitFPSPanel.setPreferredSize(new Dimension(0, 37)); - generalPanelLimitFPSPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + generalPanelCheckUpdates = + addCheckbox("Check for RSCTimes updates from GitHub at launch", generalPanel); + generalPanelCheckUpdates.setToolTipText( + "When enabled, RSCTimes will check for client updates before launching the game and install them when prompted"); - generalPanelLimitFPSCheckbox = - addCheckbox("FPS limit (doubled while F1 interlaced):", generalPanelLimitFPSPanel); - generalPanelLimitFPSCheckbox.setToolTipText( - "Limit FPS for a more 2001 feeling (or to save battery)"); + generalPanelWelcomeEnabled = + addCheckbox( + "Remind you how to open the Settings every time you log in (!!! Disable this if you know how to open the settings)", + generalPanel); + generalPanelWelcomeEnabled.setToolTipText( + "When enabled, RSCTimes will insert a message telling the current keybinding to open the settings menu and remind you about the tray icon"); - generalPanelLimitFPSSpinner = new JSpinner(); - generalPanelLimitFPSPanel.add(generalPanelLimitFPSSpinner); - generalPanelLimitFPSSpinner.setMaximumSize(new Dimension(45, 22)); - generalPanelLimitFPSSpinner.setMinimumSize(new Dimension(45, 22)); - generalPanelLimitFPSSpinner.setAlignmentY((float) 0.75); - generalPanelLimitFPSSpinner.setToolTipText("Target FPS"); - generalPanelLimitFPSSpinner.putClientProperty("JComponent.sizeVariant", "mini"); - generalPanelLimitFPSSpinner.setEnabled(false); + generalPanelCustomCursorCheckbox = addCheckbox("Use custom mouse cursor", generalPanel); + generalPanelCustomCursorCheckbox.setToolTipText( + "Switch to using a custom mouse cursor instead of the system default"); - // Sanitize JSpinner value - SpinnerNumberModel spinnerLimitFpsModel = new SpinnerNumberModel(); - spinnerLimitFpsModel.setMinimum(1); - spinnerLimitFpsModel.setMaximum(50); - spinnerLimitFpsModel.setValue(10); - spinnerLimitFpsModel.setStepSize(1); - generalPanelLimitFPSSpinner.setModel(spinnerLimitFpsModel); - ////// + generalPanelShiftScrollCameraRotationCheckbox = + addCheckbox("Enable camera rotation with compatible trackpads", generalPanel); + generalPanelShiftScrollCameraRotationCheckbox.setToolTipText( + "Trackpads that send SHIFT-SCROLL WHEEL when swiping left or right with two fingers will be able to rotate the camera"); + + JPanel generalPanelTrackPadRotationPanel = new JPanel(); + generalPanel.add(generalPanelTrackPadRotationPanel); + generalPanelTrackPadRotationPanel.setLayout( + new BoxLayout(generalPanelTrackPadRotationPanel, BoxLayout.Y_AXIS)); + generalPanelTrackPadRotationPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + + JLabel generalPanelTrackpadRotationLabel = new JLabel("Camera rotation trackpad sensitivity"); + generalPanelTrackpadRotationLabel.setToolTipText( + "Sets the camera rotation trackpad sensitivity (Default: 8)"); + generalPanelTrackPadRotationPanel.add(generalPanelTrackpadRotationLabel); + generalPanelTrackpadRotationLabel.setAlignmentY(1.0f); + + if (Util.isUsingFlatLAFTheme()) { + generalPanelTrackPadRotationPanel.add(Box.createRigidArea(osScaleMul(new Dimension(0, 10)))); + } - JPanel generalPanelLogVerbosityPanel = new JPanel(); - generalPanelLogVerbosityPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - generalPanelLogVerbosityPanel.setMaximumSize(new Dimension(350, 128)); - generalPanelLogVerbosityPanel.setLayout( - new BoxLayout(generalPanelLogVerbosityPanel, BoxLayout.Y_AXIS)); - generalPanel.add(generalPanelLogVerbosityPanel); + generalPanelTrackpadRotationSlider = new JSlider(); - JLabel generalPanelLogVerbosityTitle = new JLabel("Log verbosity maximum"); - generalPanelLogVerbosityTitle.setToolTipText( - "What max level of log text will be shown in the rsctimes log/console"); - generalPanelLogVerbosityPanel.add(generalPanelLogVerbosityTitle); - generalPanelLogVerbosityTitle.setAlignmentY((float) 0.9); + generalPanelTrackPadRotationPanel.add(generalPanelTrackpadRotationSlider); + generalPanelTrackpadRotationSlider.setAlignmentX(Component.LEFT_ALIGNMENT); + generalPanelTrackpadRotationSlider.setMaximumSize(osScaleMul(new Dimension(200, 55))); + generalPanelTrackpadRotationSlider.setBorder( + BorderFactory.createEmptyBorder(0, 0, osScaleMul(10), 0)); + generalPanelTrackpadRotationSlider.setMajorTickSpacing(2); + generalPanelTrackpadRotationSlider.setMinorTickSpacing(1); + generalPanelTrackpadRotationSlider.setMinimum(0); + generalPanelTrackpadRotationSlider.setMaximum(16); + generalPanelTrackpadRotationSlider.setPaintTicks(true); - Hashtable generalPanelLogVerbosityLabelTable = + Hashtable generalPanelTrackpadRotationLabelTable = new Hashtable(); - generalPanelLogVerbosityLabelTable.put(new Integer(0), new JLabel("Error")); - generalPanelLogVerbosityLabelTable.put(new Integer(1), new JLabel("Warning")); - generalPanelLogVerbosityLabelTable.put(new Integer(2), new JLabel("Game")); - generalPanelLogVerbosityLabelTable.put(new Integer(3), new JLabel("Info")); - generalPanelLogVerbosityLabelTable.put(new Integer(4), new JLabel("Debug")); - generalPanelLogVerbosityLabelTable.put(new Integer(5), new JLabel("Opcode")); + generalPanelTrackpadRotationLabelTable.put(new Integer(0), new JLabel("0")); + generalPanelTrackpadRotationLabelTable.put(new Integer(4), new JLabel("4")); + generalPanelTrackpadRotationLabelTable.put(new Integer(8), new JLabel("8")); + generalPanelTrackpadRotationLabelTable.put(new Integer(12), new JLabel("12")); + generalPanelTrackpadRotationLabelTable.put(new Integer(16), new JLabel("16")); + generalPanelTrackpadRotationSlider.setLabelTable(generalPanelTrackpadRotationLabelTable); + generalPanelTrackpadRotationSlider.setPaintLabels(true); - generalPanelLogVerbositySlider = new JSlider(); - generalPanelLogVerbositySlider.setMajorTickSpacing(1); - generalPanelLogVerbositySlider.setLabelTable(generalPanelLogVerbosityLabelTable); - generalPanelLogVerbositySlider.setPaintLabels(true); - generalPanelLogVerbositySlider.setPaintTicks(true); - generalPanelLogVerbositySlider.setSnapToTicks(true); - generalPanelLogVerbositySlider.setMinimum(0); - generalPanelLogVerbositySlider.setMaximum(5); - generalPanelLogVerbositySlider.setPreferredSize(new Dimension(200, 55)); - generalPanelLogVerbositySlider.setAlignmentX(Component.LEFT_ALIGNMENT); - generalPanelLogVerbositySlider.setBorder(new EmptyBorder(0, 0, 5, 0)); - generalPanelLogVerbositySlider.setOrientation(SwingConstants.HORIZONTAL); - generalPanelLogVerbosityPanel.add(generalPanelLogVerbositySlider); - - generalPanelLogTimestampsCheckbox = addCheckbox("Show timestamps in log", generalPanel); - generalPanelLogTimestampsCheckbox.setToolTipText( - "Displays the time text was output to the log"); - - generalPanelLogLevelCheckbox = addCheckbox("Show log level in log", generalPanel); - generalPanelLogLevelCheckbox.setToolTipText("Displays the log level of output in the log"); - - generalPanelLogForceTimestampsCheckbox = addCheckbox("Force timestamps in log", generalPanel); - generalPanelLogForceTimestampsCheckbox.setToolTipText( - "Forces display of the time text was output to the log"); - - generalPanelLogForceLevelCheckbox = addCheckbox("Force log level in log", generalPanel); - generalPanelLogForceLevelCheckbox.setToolTipText( - "Forces display of the log level of output in the log"); - - generalPanelColoredTextCheckbox = addCheckbox("Colored console text", generalPanel); - generalPanelColoredTextCheckbox.setToolTipText( - "When running the client from a console, chat messages in the console will reflect the colors they are in game"); + generalPanelAutoScreenshotCheckbox = + addCheckbox("Take a screenshot when you level up or complete a quest", generalPanel); + generalPanelAutoScreenshotCheckbox.setToolTipText( + "Takes a screenshot for you for level ups and quest completion"); generalPanelDebugModeCheckbox = addCheckbox("Enable debug mode", generalPanel); generalPanelDebugModeCheckbox.setToolTipText( @@ -703,7 +1091,6 @@ public void actionPerformed(ActionEvent e) { addCheckbox("Combat style menu shown outside of combat", generalPanel); generalPanelCombatXPMenuCheckbox.setToolTipText( "Always show the combat style menu when out of combat"); - generalPanelCombatXPMenuCheckbox.setBorder(new EmptyBorder(7, 0, 10, 0)); generalPanelCombatXPMenuCheckbox.setEnabled(false); generalPanelCombatXPMenuHiddenCheckbox = @@ -729,11 +1116,6 @@ public void actionPerformed(ActionEvent e) { generalPanelRoofHidingCheckbox = addCheckbox("Roof hiding", generalPanel); generalPanelRoofHidingCheckbox.setToolTipText("Always hide rooftops"); - generalPanelDisableUndergroundLightingCheckbox = - addCheckbox("Disable underground lighting flicker", generalPanel); - generalPanelDisableUndergroundLightingCheckbox.setToolTipText( - "Underground will no longer flicker, basically"); - generalPanelCameraZoomableCheckbox = addCheckbox("Camera zoom enhancement", generalPanel); generalPanelCameraZoomableCheckbox.setToolTipText( "Zoom the camera in and out with the mouse wheel, and no longer zooms in inside buildings"); @@ -751,11 +1133,113 @@ public void actionPerformed(ActionEvent e) { generalPanelCameraMovableRelativeCheckbox.setToolTipText( "Camera movement will follow the player position"); + addSettingsHeader(generalPanel, "Graphical effect changes"); + + JPanel generalPanelViewDistancePanel = new JPanel(); + generalPanel.add(generalPanelViewDistancePanel); + generalPanelViewDistancePanel.setLayout( + new BoxLayout(generalPanelViewDistancePanel, BoxLayout.Y_AXIS)); + generalPanelViewDistancePanel.setAlignmentX(Component.LEFT_ALIGNMENT); + + JLabel generalPanelViewDistanceLabel = new JLabel("View distance"); + generalPanelViewDistanceLabel.setToolTipText( + "Sets the max render distance of structures and landscape"); + generalPanelViewDistancePanel.add(generalPanelViewDistanceLabel); + generalPanelViewDistanceLabel.setAlignmentY(1.0f); + + if (Util.isUsingFlatLAFTheme()) { + generalPanelViewDistancePanel.add(Box.createRigidArea(osScaleMul(new Dimension(0, 5)))); + } + + generalPanelViewDistanceSlider = new JSlider(); + + generalPanelViewDistancePanel.add(generalPanelViewDistanceSlider); + generalPanelViewDistanceSlider.setAlignmentX(Component.LEFT_ALIGNMENT); + generalPanelViewDistanceSlider.setMaximumSize(osScaleMul(new Dimension(350, 55))); + generalPanelViewDistanceSlider.setBorder( + BorderFactory.createEmptyBorder(0, 0, osScaleMul(10), 0)); + generalPanelViewDistanceSlider.setMinorTickSpacing(500); + generalPanelViewDistanceSlider.setMajorTickSpacing(1000); + generalPanelViewDistanceSlider.setMinimum(2300); + generalPanelViewDistanceSlider.setMaximum(20000); + generalPanelViewDistanceSlider.setPaintTicks(true); + + Hashtable generalPanelViewDistanceLabelTable = + new Hashtable(); + generalPanelViewDistanceLabelTable.put(new Integer(2300), new JLabel("2,300")); + generalPanelViewDistanceLabelTable.put(new Integer(10000), new JLabel("10,000")); + generalPanelViewDistanceLabelTable.put(new Integer(20000), new JLabel("20,000")); + generalPanelViewDistanceSlider.setLabelTable(generalPanelViewDistanceLabelTable); + generalPanelViewDistanceSlider.setPaintLabels(true); + ////// + + // FOV slider + JPanel generalPanelFoVPanel = new JPanel(); + generalPanel.add(generalPanelFoVPanel); + generalPanelFoVPanel.setLayout(new BoxLayout(generalPanelFoVPanel, BoxLayout.Y_AXIS)); + generalPanelFoVPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + + JLabel generalPanelFoVLabel = new JLabel("Field of view (Default 9)"); + generalPanelFoVLabel.setToolTipText("Sets the field of view (not recommended past 10)"); + generalPanelFoVPanel.add(generalPanelFoVLabel); + generalPanelFoVLabel.setAlignmentY(1.0f); + + if (Util.isUsingFlatLAFTheme()) { + generalPanelFoVPanel.add(Box.createRigidArea(osScaleMul(new Dimension(0, 5)))); + } + + generalPanelFoVSlider = new JSlider(); + + generalPanelFoVPanel.add(generalPanelFoVSlider); + generalPanelFoVSlider.setAlignmentX(Component.LEFT_ALIGNMENT); + generalPanelFoVSlider.setMaximumSize(osScaleMul(new Dimension(300, 55))); + generalPanelFoVSlider.setBorder(BorderFactory.createEmptyBorder(0, 0, osScaleMul(10), 0)); + generalPanelFoVSlider.setMinimum(7); + generalPanelFoVSlider.setMaximum(16); + generalPanelFoVSlider.setMajorTickSpacing(1); + generalPanelFoVSlider.setPaintTicks(true); + generalPanelFoVSlider.setPaintLabels(true); + ////// + + generalPanelDisableUndergroundLightingCheckbox = + addCheckbox("Disable underground lighting flicker", generalPanel); + generalPanelDisableUndergroundLightingCheckbox.setToolTipText( + "Underground will no longer flicker, basically"); + + // FPS limit + JPanel generalPanelLimitFPSPanel = new JPanel(); + generalPanel.add(generalPanelLimitFPSPanel); + generalPanelLimitFPSPanel.setLayout(new BoxLayout(generalPanelLimitFPSPanel, BoxLayout.X_AXIS)); + generalPanelLimitFPSPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + + generalPanelLimitFPSCheckbox = + addCheckbox("FPS limit (doubled while F1 interlaced):", generalPanelLimitFPSPanel); + generalPanelLimitFPSCheckbox.setToolTipText( + "Limit FPS for a more 2001 feeling (or to save battery)"); + + generalPanelLimitFPSSpinner = new JSpinner(); + generalPanelLimitFPSPanel.add(generalPanelLimitFPSSpinner); + generalPanelLimitFPSSpinner.setMaximumSize(osScaleMul(new Dimension(50, 22))); + generalPanelLimitFPSSpinner.setMinimumSize(osScaleMul(new Dimension(50, 22))); + generalPanelLimitFPSSpinner.setAlignmentY(0.75f); + generalPanelLimitFPSSpinner.setToolTipText("Target FPS"); + generalPanelLimitFPSSpinner.putClientProperty("JComponent.sizeVariant", "mini"); + generalPanelLimitFPSSpinner.setEnabled(false); + + // Sanitize JSpinner value + SpinnerNumberModel spinnerLimitFpsModel = new SpinnerNumberModel(); + spinnerLimitFpsModel.setMinimum(1); + spinnerLimitFpsModel.setMaximum(50); + spinnerLimitFpsModel.setValue(10); + spinnerLimitFpsModel.setStepSize(1); + generalPanelLimitFPSSpinner.setModel(spinnerLimitFpsModel); + ////// + addSettingsHeader(generalPanel, "Menu/Item patching"); JPanel generalPanelNamePatchModePanel = new JPanel(); generalPanelNamePatchModePanel.setAlignmentX(Component.LEFT_ALIGNMENT); - generalPanelNamePatchModePanel.setMaximumSize(new Dimension(300, 60)); + generalPanelNamePatchModePanel.setMaximumSize(osScaleMul(new Dimension(300, 85))); generalPanelNamePatchModePanel.setLayout( new BoxLayout(generalPanelNamePatchModePanel, BoxLayout.X_AXIS)); generalPanel.add(generalPanelNamePatchModePanel); @@ -767,17 +1251,19 @@ public void actionPerformed(ActionEvent e) { generalPanelNamePatchModeSlider.setSnapToTicks(true); generalPanelNamePatchModeSlider.setMinimum(0); generalPanelNamePatchModeSlider.setMaximum(3); - generalPanelNamePatchModeSlider.setPreferredSize(new Dimension(33, 0)); + generalPanelNamePatchModeSlider.setPreferredSize(osScaleMul(new Dimension(40, 0))); generalPanelNamePatchModeSlider.setAlignmentX(Component.LEFT_ALIGNMENT); - generalPanelNamePatchModeSlider.setBorder(new EmptyBorder(7, 0, 10, 0)); + generalPanelNamePatchModeSlider.setBorder( + BorderFactory.createEmptyBorder(0, 0, osScaleMul(10), 0)); generalPanelNamePatchModeSlider.setOrientation(SwingConstants.VERTICAL); generalPanelNamePatchModePanel.add(generalPanelNamePatchModeSlider); generalPanelNamePatchModeSlider.setEnabled(false); JPanel generalPanelNamePatchModeTextPanel = new JPanel(); - generalPanelNamePatchModeTextPanel.setPreferredSize(new Dimension(255, 80)); + generalPanelNamePatchModeTextPanel.setPreferredSize(osScaleMul(new Dimension(255, 80))); generalPanelNamePatchModeTextPanel.setLayout(new BorderLayout()); - generalPanelNamePatchModeTextPanel.setBorder(new EmptyBorder(7, 10, 0, 0)); + generalPanelNamePatchModeTextPanel.setBorder( + BorderFactory.createEmptyBorder(0, osScaleMul(10), 0, 0)); generalPanelNamePatchModePanel.add(generalPanelNamePatchModeTextPanel); JLabel generalPanelNamePatchModeTitle = @@ -832,6 +1318,91 @@ public void stateChanged(ChangeEvent e) { "Even in the very last versions of the client, the horizontal blue bar at the bottom was still misaligned so that 1 pixel shines through at the end"); generalPanelPatchHbar512LastPixelCheckbox.setEnabled(false); + // Logger settings + + addSettingsHeader(generalPanel, "Logging settings"); + + JPanel generalPanelLogVerbosityPanel = new JPanel(); + generalPanelLogVerbosityPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + generalPanelLogVerbosityPanel.setMaximumSize(osScaleMul(new Dimension(350, 128))); + generalPanelLogVerbosityPanel.setLayout( + new BoxLayout(generalPanelLogVerbosityPanel, BoxLayout.Y_AXIS)); + generalPanel.add(generalPanelLogVerbosityPanel); + + JLabel generalPanelLogVerbosityTitle = new JLabel("Log verbosity maximum"); + generalPanelLogVerbosityTitle.setToolTipText( + "What max level of log text will be shown in the RSCTimes log/console"); + generalPanelLogVerbosityPanel.add(generalPanelLogVerbosityTitle); + generalPanelLogVerbosityTitle.setAlignmentY(1.0f); + + Hashtable generalPanelLogVerbosityLabelTable = + new Hashtable(); + generalPanelLogVerbosityLabelTable.put(new Integer(0), new JLabel("Error")); + generalPanelLogVerbosityLabelTable.put(new Integer(1), new JLabel("Warning")); + generalPanelLogVerbosityLabelTable.put(new Integer(2), new JLabel("Game")); + generalPanelLogVerbosityLabelTable.put(new Integer(3), new JLabel("Info")); + generalPanelLogVerbosityLabelTable.put(new Integer(4), new JLabel("Debug")); + generalPanelLogVerbosityLabelTable.put(new Integer(5), new JLabel("Opcode")); + + if (Util.isUsingFlatLAFTheme()) { + generalPanelLogVerbosityPanel.add(Box.createRigidArea(osScaleMul(new Dimension(0, 5)))); + } + + generalPanelLogVerbositySlider = new JSlider(); + generalPanelLogVerbositySlider.setMajorTickSpacing(1); + generalPanelLogVerbositySlider.setLabelTable(generalPanelLogVerbosityLabelTable); + generalPanelLogVerbositySlider.setPaintLabels(true); + generalPanelLogVerbositySlider.setPaintTicks(true); + generalPanelLogVerbositySlider.setSnapToTicks(true); + generalPanelLogVerbositySlider.setMinimum(0); + generalPanelLogVerbositySlider.setMaximum(5); + generalPanelLogVerbositySlider.setPreferredSize(osScaleMul(new Dimension(200, 55))); + generalPanelLogVerbositySlider.setAlignmentX(Component.LEFT_ALIGNMENT); + generalPanelLogVerbositySlider.setBorder( + BorderFactory.createEmptyBorder(0, 0, osScaleMul(5), 0)); + generalPanelLogVerbositySlider.setOrientation(SwingConstants.HORIZONTAL); + generalPanelLogVerbosityPanel.add(generalPanelLogVerbositySlider); + + generalPanelLogTimestampsCheckbox = addCheckbox("Show timestamps in log", generalPanel); + generalPanelLogTimestampsCheckbox.setToolTipText( + "Displays the time text was output to the log"); + + generalPanelLogLevelCheckbox = addCheckbox("Show log level in log", generalPanel); + generalPanelLogLevelCheckbox.setToolTipText("Displays the log level of output in the log"); + + generalPanelLogForceTimestampsCheckbox = addCheckbox("Force timestamps in log", generalPanel); + generalPanelLogForceTimestampsCheckbox.setToolTipText( + "Forces display of the time text was output to the log"); + + generalPanelLogForceLevelCheckbox = addCheckbox("Force log level in log", generalPanel); + generalPanelLogForceLevelCheckbox.setToolTipText( + "Forces display of the log level of output in the log"); + + generalPanelColoredTextCheckbox = addCheckbox("Coloured console text", generalPanel); + generalPanelColoredTextCheckbox.setToolTipText( + "When running the client from a console, chat messages in the console will reflect the colours they are in game"); + + // UI Settings + addSettingsHeader(generalPanel, "UI settings"); + + generalPanelUseDarkModeCheckbox = + addCheckbox( + "Use dark mode for the interface (Requires restart & modern UI theme)", generalPanel); + generalPanelUseDarkModeCheckbox.setToolTipText( + "Uses the darker UI theme, unless the legacy theme is enabled"); + + generalPanelUseNimbusThemeCheckbox = + addCheckbox("Use legacy RSCx UI theme (Requires restart)", generalPanel); + generalPanelUseNimbusThemeCheckbox.setToolTipText("Uses the legacy RSCx Nimbus look-and-feel"); + + if (Util.isModernWindowsOS() && Launcher.OSScalingFactor != 1.0) { + generalPanelUseNimbusThemeCheckbox.setEnabled(false); + generalPanelUseNimbusThemeCheckbox.setText( + "Use legacy RSCx UI theme You must disable OS level scaling in Windows to enable this option (Requires restart)"); + } + + addPanelBottomGlue(generalPanel); + /* * Overlays tab */ @@ -843,7 +1414,6 @@ public void stateChanged(ChangeEvent e) { addSettingsHeader(overlayPanel, "Interface Overlays"); overlayPanelStatusDisplayCheckbox = addCheckbox("Show HP display", overlayPanel); overlayPanelStatusDisplayCheckbox.setToolTipText("Toggle hits display"); - overlayPanelStatusDisplayCheckbox.setBorder(new EmptyBorder(0, 0, 10, 0)); overlayPanelBuffsCheckbox = addCheckbox("Show combat (de)buffs and cooldowns display", overlayPanel); @@ -875,11 +1445,10 @@ public void stateChanged(ChangeEvent e) { overlayPanel.add(overlayPanelRscTimesButtonsPanel); overlayPanelRscTimesButtonsPanel.setLayout( new BoxLayout(overlayPanelRscTimesButtonsPanel, BoxLayout.X_AXIS)); - overlayPanelRscTimesButtonsPanel.setPreferredSize(new Dimension(0, 37)); overlayPanelRscTimesButtonsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - overlayPanelRscTimesButtonsPanel.setBorder(new EmptyBorder(0, 0, 0, 0)); + overlayPanelRscTimesButtonsPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0)); JLabel rsctimesButtonsSpacingLabel = new JLabel(""); - rsctimesButtonsSpacingLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 20)); + rsctimesButtonsSpacingLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, osScaleMul(20))); overlayPanelRscTimesButtonsPanel.add(rsctimesButtonsSpacingLabel); overlayPanelRscTimesButtonsCheckbox = addCheckbox( @@ -913,14 +1482,14 @@ public void stateChanged(ChangeEvent e) { addSettingsHeader(overlayPanel, "XP Bar"); overlayPanelXPBarCheckbox = addCheckbox("Show a Goal bar", overlayPanel); overlayPanelXPBarCheckbox.setToolTipText("Show a Goal bar to the left of the wrench"); - overlayPanelXPBarCheckbox.setBorder(new EmptyBorder(7, 0, 10, 0)); ButtonGroup XPAlignButtonGroup = new ButtonGroup(); - overlayPanelXPRightAlignFocusButton = addRadioButton("Display on the right", overlayPanel, 20); + overlayPanelXPRightAlignFocusButton = + addRadioButton("Display on the right", overlayPanel, osScaleMul(20)); overlayPanelXPRightAlignFocusButton.setToolTipText( "The Goal bar will be shown just left of the Settings menu."); overlayPanelXPCenterAlignFocusButton = - addRadioButton("Display in the center", overlayPanel, 20); + addRadioButton("Display in the center", overlayPanel, osScaleMul(20)); overlayPanelXPCenterAlignFocusButton.setToolTipText( "The Goal bar will be shown at the top-middle of the screen."); XPAlignButtonGroup.add(overlayPanelXPRightAlignFocusButton); @@ -943,7 +1512,7 @@ public void stateChanged(ChangeEvent e) { overlayPanelLagIndicatorCheckbox = addCheckbox("Lag indicator", overlayPanel); overlayPanelLagIndicatorCheckbox.setToolTipText( - "When there's a problem with your connection, rsctimes will tell you in the bottom right"); + "When there's a problem with your connection, RSCTimes will tell you in the bottom right"); overlayPanelFoodHealingCheckbox = addCheckbox("Show food healing overlay (Not implemented yet)", overlayPanel); @@ -966,7 +1535,6 @@ public void stateChanged(ChangeEvent e) { addCheckbox("Show hitboxes around NPCs, players, and items", overlayPanel); overlayPanelHitboxCheckbox.setToolTipText( "Shows the clickable areas on NPCs, players, and items"); - overlayPanelHitboxCheckbox.setBorder(new EmptyBorder(0, 0, 10, 0)); overlayPanelPlayerNamesCheckbox = addCheckbox("Show player names over their heads", overlayPanel); @@ -995,41 +1563,45 @@ public void stateChanged(ChangeEvent e) { addCheckbox("Display the names of items on the ground", overlayPanel); overlayPanelItemNamesCheckbox.setToolTipText("Shows the names of dropped items"); + int itemsTextHeight = isUsingFlatLAFTheme() ? 32 : 37; + // Blocked Items JPanel blockedItemsPanel = new JPanel(); overlayPanel.add(blockedItemsPanel); blockedItemsPanel.setLayout(new BoxLayout(blockedItemsPanel, BoxLayout.X_AXIS)); - blockedItemsPanel.setPreferredSize(new Dimension(0, 37)); + blockedItemsPanel.setPreferredSize(osScaleMul(new Dimension(0, itemsTextHeight))); blockedItemsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - blockedItemsPanel.setBorder(new EmptyBorder(0, 0, 9, 0)); + blockedItemsPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, osScaleMul(9), 0)); JLabel blockedItemsPanelNameLabel = new JLabel("Blocked items: "); blockedItemsPanel.add(blockedItemsPanelNameLabel); - blockedItemsPanelNameLabel.setAlignmentY((float) 0.9); + blockedItemsPanelNameLabel.setAlignmentY(0.9f); blockedItemsTextField = new JTextField(); blockedItemsPanel.add(blockedItemsTextField); - blockedItemsTextField.setMinimumSize(new Dimension(100, 28)); - blockedItemsTextField.setMaximumSize(new Dimension(Short.MAX_VALUE, 28)); - blockedItemsTextField.setAlignmentY((float) 0.75); + blockedItemsTextField.setMinimumSize(osScaleMul(new Dimension(100, 28))); + blockedItemsTextField.setMaximumSize(new Dimension(Short.MAX_VALUE, osScaleMul(28))); + blockedItemsTextField.setAlignmentY(0.75f); // Highlighted Items JPanel highlightedItemsPanel = new JPanel(); overlayPanel.add(highlightedItemsPanel); highlightedItemsPanel.setLayout(new BoxLayout(highlightedItemsPanel, BoxLayout.X_AXIS)); - highlightedItemsPanel.setPreferredSize(new Dimension(0, 37)); + highlightedItemsPanel.setPreferredSize(osScaleMul(new Dimension(0, itemsTextHeight))); highlightedItemsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - highlightedItemsPanel.setBorder(new EmptyBorder(0, 0, 9, 0)); + highlightedItemsPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, osScaleMul(9), 0)); JLabel highlightedItemsPanelNameLabel = new JLabel("Highlighted items: "); highlightedItemsPanel.add(highlightedItemsPanelNameLabel); - highlightedItemsPanelNameLabel.setAlignmentY((float) 0.9); + highlightedItemsPanelNameLabel.setAlignmentY(0.9f); highlightedItemsTextField = new JTextField(); highlightedItemsPanel.add(highlightedItemsTextField); - highlightedItemsTextField.setMinimumSize(new Dimension(100, 28)); - highlightedItemsTextField.setMaximumSize(new Dimension(Short.MAX_VALUE, 28)); - highlightedItemsTextField.setAlignmentY((float) 0.75); + highlightedItemsTextField.setMinimumSize(osScaleMul(new Dimension(100, 28))); + highlightedItemsTextField.setMaximumSize(new Dimension(Short.MAX_VALUE, osScaleMul(28))); + highlightedItemsTextField.setAlignmentY(0.75f); + + addPanelBottomGlue(overlayPanel); /* * Notifications tab @@ -1041,29 +1613,31 @@ public void stateChanged(ChangeEvent e) { notificationPanelTrayPopupCheckbox = addCheckbox("Enable notification tray popups", notificationPanel); - notificationPanelTrayPopupCheckbox.setBorder(BorderFactory.createEmptyBorder(0, 0, 7, 0)); + notificationPanelTrayPopupCheckbox.setBorder( + BorderFactory.createEmptyBorder(0, 0, osScaleMul(7), 0)); notificationPanelTrayPopupCheckbox.setToolTipText( "Shows a system notification when a notification is triggered"); ButtonGroup trayPopupButtonGroup = new ButtonGroup(); notificationPanelTrayPopupClientFocusButton = - addRadioButton("Only when client is not focused", notificationPanel, 20); + addRadioButton("Only when client is not focused", notificationPanel, osScaleMul(20)); notificationPanelTrayPopupAnyFocusButton = - addRadioButton("Regardless of client focus", notificationPanel, 20); + addRadioButton("Regardless of client focus", notificationPanel, osScaleMul(20)); trayPopupButtonGroup.add(notificationPanelTrayPopupClientFocusButton); trayPopupButtonGroup.add(notificationPanelTrayPopupAnyFocusButton); notificationPanelNotifSoundsCheckbox = addCheckbox("Enable notification sounds", notificationPanel); - notificationPanelNotifSoundsCheckbox.setBorder(BorderFactory.createEmptyBorder(0, 0, 7, 0)); + notificationPanelNotifSoundsCheckbox.setBorder( + BorderFactory.createEmptyBorder(0, 0, osScaleMul(7), 0)); notificationPanelNotifSoundsCheckbox.setToolTipText( "Plays a sound when a notification is triggered"); ButtonGroup notifSoundButtonGroup = new ButtonGroup(); notificationPanelNotifSoundClientFocusButton = - addRadioButton("Only when client is not focused", notificationPanel, 20); + addRadioButton("Only when client is not focused", notificationPanel, osScaleMul(20)); notificationPanelNotifSoundAnyFocusButton = - addRadioButton("Regardless of client focus", notificationPanel, 20); + addRadioButton("Regardless of client focus", notificationPanel, osScaleMul(20)); notifSoundButtonGroup.add(notificationPanelNotifSoundClientFocusButton); notifSoundButtonGroup.add(notificationPanelNotifSoundAnyFocusButton); @@ -1081,7 +1655,8 @@ public void stateChanged(ChangeEvent e) { addSettingsHeader(notificationPanel, "Notifications"); notificationPanelPMNotifsCheckbox = addCheckbox("Enable PM notifications", notificationPanel); - notificationPanelPMNotifsCheckbox.setBorder(BorderFactory.createEmptyBorder(0, 0, 7, 0)); + notificationPanelPMNotifsCheckbox.setBorder( + BorderFactory.createEmptyBorder(0, 0, osScaleMul(7), 0)); notificationPanelPMNotifsCheckbox.setToolTipText( "Shows a system notification when a PM is received"); @@ -1095,16 +1670,11 @@ public void stateChanged(ChangeEvent e) { notificationPanelUnderAttackNotifsCheckbox.setToolTipText( "Shows a system notification when a player attacks you"); - notificationPanelLogoutNotifsCheckbox = - addCheckbox("Enable logout notification", notificationPanel); - notificationPanelLogoutNotifsCheckbox.setToolTipText( - "Shows a system notification when about to idle out"); - JPanel notificationPanelLowHPNotifsPanel = new JPanel(); notificationPanel.add(notificationPanelLowHPNotifsPanel); notificationPanelLowHPNotifsPanel.setLayout( new BoxLayout(notificationPanelLowHPNotifsPanel, BoxLayout.X_AXIS)); - notificationPanelLowHPNotifsPanel.setPreferredSize(new Dimension(0, 37)); + notificationPanelLowHPNotifsPanel.setPreferredSize(osScaleMul(new Dimension(0, 28))); notificationPanelLowHPNotifsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); notificationPanelLowHPNotifsCheckbox = @@ -1113,16 +1683,17 @@ public void stateChanged(ChangeEvent e) { "Shows a system notification when your HP drops below the specified value"); notificationPanelLowHPNotifsSpinner = new JSpinner(); - notificationPanelLowHPNotifsSpinner.setMaximumSize(new Dimension(45, 22)); - notificationPanelLowHPNotifsSpinner.setMinimumSize(new Dimension(45, 22)); - notificationPanelLowHPNotifsSpinner.setAlignmentY((float) 0.75); + notificationPanelLowHPNotifsSpinner.setMaximumSize(osScaleMul(new Dimension(55, 22))); + notificationPanelLowHPNotifsSpinner.setMinimumSize(osScaleMul(new Dimension(55, 22))); + notificationPanelLowHPNotifsSpinner.setAlignmentY(0.75f); notificationPanelLowHPNotifsPanel.add(notificationPanelLowHPNotifsSpinner); notificationPanelLowHPNotifsSpinner.putClientProperty("JComponent.sizeVariant", "mini"); JLabel notificationPanelLowHPNotifsEndLabel = new JLabel("% HP"); notificationPanelLowHPNotifsPanel.add(notificationPanelLowHPNotifsEndLabel); - notificationPanelLowHPNotifsEndLabel.setAlignmentY((float) 0.9); - notificationPanelLowHPNotifsEndLabel.setBorder(new EmptyBorder(0, 2, 0, 0)); + notificationPanelLowHPNotifsEndLabel.setAlignmentY(0.8f); + notificationPanelLowHPNotifsEndLabel.setBorder( + BorderFactory.createEmptyBorder(0, osScaleMul(2), 0, 0)); // Sanitize JSpinner values SpinnerNumberModel spinnerHPNumModel = new SpinnerNumberModel(); @@ -1136,15 +1707,17 @@ public void stateChanged(ChangeEvent e) { notificationPanel.add(warnHighlightedOnGroundPanel); warnHighlightedOnGroundPanel.setLayout( new BoxLayout(warnHighlightedOnGroundPanel, BoxLayout.X_AXIS)); - warnHighlightedOnGroundPanel.setPreferredSize(new Dimension(0, 37)); + warnHighlightedOnGroundPanel.setPreferredSize(osScaleMul(new Dimension(0, 28))); warnHighlightedOnGroundPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + // TODO: implement feature, remove "setEnabled(false)" below notificationPanelHighlightedItemTimerCheckbox = addCheckbox( "Warn if one of your highlighted items has been on the ground for more than", warnHighlightedOnGroundPanel); notificationPanelHighlightedItemTimerCheckbox.setToolTipText( "Highlighted items can be configured in the Overlays tab"); + notificationPanelHighlightedItemTimerCheckbox.setEnabled(false); JLabel highlightedItemsSuggestionJLabel = new JLabel( @@ -1152,20 +1725,26 @@ public void stateChanged(ChangeEvent e) { + "Note: Loot from kills despawns after about 2 minutes." + "

"); notificationPanel.add(highlightedItemsSuggestionJLabel); - highlightedItemsSuggestionJLabel.setBorder(new EmptyBorder(0, 0, 8, 0)); + highlightedItemsSuggestionJLabel.setBorder( + BorderFactory.createEmptyBorder(0, 0, osScaleMul(8), 0)); + highlightedItemsSuggestionJLabel.setEnabled(false); notificationPanelHighlightedItemTimerSpinner = new JSpinner(); - notificationPanelHighlightedItemTimerSpinner.setMaximumSize(new Dimension(55, 22)); - notificationPanelHighlightedItemTimerSpinner.setMinimumSize(new Dimension(55, 22)); - notificationPanelHighlightedItemTimerSpinner.setAlignmentY((float) 0.75); + notificationPanelHighlightedItemTimerSpinner.setMaximumSize(osScaleMul(new Dimension(65, 22))); + notificationPanelHighlightedItemTimerSpinner.setMinimumSize(osScaleMul(new Dimension(65, 22))); + notificationPanelHighlightedItemTimerSpinner.setAlignmentY(0.75f); warnHighlightedOnGroundPanel.add(notificationPanelHighlightedItemTimerSpinner); notificationPanelHighlightedItemTimerSpinner.putClientProperty( "JComponent.sizeVariant", "mini"); + notificationPanelHighlightedItemTimerSpinner.setEnabled(false); JLabel notificationPanelHighlightedItemEndLabel = new JLabel("seconds"); warnHighlightedOnGroundPanel.add(notificationPanelHighlightedItemEndLabel); - notificationPanelHighlightedItemEndLabel.setAlignmentY((float) 0.9); - notificationPanelHighlightedItemEndLabel.setBorder(new EmptyBorder(0, 2, 0, 0)); + notificationPanelHighlightedItemEndLabel.setAlignmentY(0.8f); + int secondsMargin = isUsingFlatLAFTheme() ? 4 : 2; + notificationPanelHighlightedItemEndLabel.setBorder( + BorderFactory.createEmptyBorder(0, osScaleMul(secondsMargin), 0, 0)); + notificationPanelHighlightedItemEndLabel.setEnabled(false); // Sanitize JSpinner values SpinnerNumberModel highlightedItemSecondsModel = new SpinnerNumberModel(); @@ -1174,6 +1753,8 @@ public void stateChanged(ChangeEvent e) { highlightedItemSecondsModel.setValue(100); notificationPanelHighlightedItemTimerSpinner.setModel(highlightedItemSecondsModel); + addPanelBottomGlue(notificationPanel); + /* * Streaming & Privacy tab */ @@ -1186,69 +1767,77 @@ public void stateChanged(ChangeEvent e) { addCheckbox("Enable Twitch chat integration", streamingPanel); streamingPanelTwitchChatIntegrationEnabledCheckbox.setToolTipText( "If this box is checked, and the 3 relevant text fields are filled out, you will connect to a chat channel on login."); - streamingPanelTwitchChatIntegrationEnabledCheckbox.setBorder(new EmptyBorder(0, 0, 7, 0)); streamingPanelTwitchChatCheckbox = addCheckbox("Hide incoming Twitch chat", streamingPanel); streamingPanelTwitchChatCheckbox.setToolTipText( "Don't show chat from other Twitch users, but still be able to send Twitch chat"); + int twitchTextHeight = isUsingFlatLAFTheme() ? 32 : 37; + JPanel streamingPanelTwitchChannelNamePanel = new JPanel(); streamingPanel.add(streamingPanelTwitchChannelNamePanel); streamingPanelTwitchChannelNamePanel.setLayout( new BoxLayout(streamingPanelTwitchChannelNamePanel, BoxLayout.X_AXIS)); - streamingPanelTwitchChannelNamePanel.setPreferredSize(new Dimension(0, 37)); + streamingPanelTwitchChannelNamePanel.setPreferredSize( + osScaleMul(new Dimension(0, twitchTextHeight))); streamingPanelTwitchChannelNamePanel.setAlignmentX(Component.LEFT_ALIGNMENT); - streamingPanelTwitchChannelNamePanel.setBorder(new EmptyBorder(0, 0, 7, 0)); + streamingPanelTwitchChannelNamePanel.setBorder( + BorderFactory.createEmptyBorder(0, 0, osScaleMul(7), 0)); JLabel streamingPanelTwitchChannelNameLabel = new JLabel("Twitch channel to chat in: "); streamingPanelTwitchChannelNameLabel.setToolTipText("The Twitch channel you want to chat in"); streamingPanelTwitchChannelNamePanel.add(streamingPanelTwitchChannelNameLabel); - streamingPanelTwitchChannelNameLabel.setAlignmentY((float) 0.9); + streamingPanelTwitchChannelNameLabel.setAlignmentY(0.9f); streamingPanelTwitchChannelNameTextField = new JTextField(); streamingPanelTwitchChannelNamePanel.add(streamingPanelTwitchChannelNameTextField); - streamingPanelTwitchChannelNameTextField.setMinimumSize(new Dimension(100, 28)); - streamingPanelTwitchChannelNameTextField.setMaximumSize(new Dimension(Short.MAX_VALUE, 28)); - streamingPanelTwitchChannelNameTextField.setAlignmentY((float) 0.75); + streamingPanelTwitchChannelNameTextField.setMinimumSize(osScaleMul(new Dimension(100, 28))); + streamingPanelTwitchChannelNameTextField.setMaximumSize( + new Dimension(Short.MAX_VALUE, osScaleMul(28))); + streamingPanelTwitchChannelNameTextField.setAlignmentY(0.75f); JPanel streamingPanelTwitchUserPanel = new JPanel(); streamingPanel.add(streamingPanelTwitchUserPanel); streamingPanelTwitchUserPanel.setLayout( new BoxLayout(streamingPanelTwitchUserPanel, BoxLayout.X_AXIS)); - streamingPanelTwitchUserPanel.setPreferredSize(new Dimension(0, 37)); + streamingPanelTwitchUserPanel.setPreferredSize(osScaleMul(new Dimension(0, twitchTextHeight))); streamingPanelTwitchUserPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - streamingPanelTwitchUserPanel.setBorder(new EmptyBorder(0, 0, 7, 0)); + streamingPanelTwitchUserPanel.setBorder( + BorderFactory.createEmptyBorder(0, 0, osScaleMul(7), 0)); JLabel streamingPanelTwitchUserLabel = new JLabel("Your Twitch username: "); streamingPanelTwitchUserLabel.setToolTipText("The Twitch username you log into Twitch with"); streamingPanelTwitchUserPanel.add(streamingPanelTwitchUserLabel); - streamingPanelTwitchUserLabel.setAlignmentY((float) 0.9); + streamingPanelTwitchUserLabel.setAlignmentY(0.9f); streamingPanelTwitchUserTextField = new JTextField(); streamingPanelTwitchUserPanel.add(streamingPanelTwitchUserTextField); - streamingPanelTwitchUserTextField.setMinimumSize(new Dimension(100, 28)); - streamingPanelTwitchUserTextField.setMaximumSize(new Dimension(Short.MAX_VALUE, 28)); - streamingPanelTwitchUserTextField.setAlignmentY((float) 0.75); + streamingPanelTwitchUserTextField.setMinimumSize(osScaleMul(new Dimension(100, 28))); + streamingPanelTwitchUserTextField.setMaximumSize( + new Dimension(Short.MAX_VALUE, osScaleMul(28))); + streamingPanelTwitchUserTextField.setAlignmentY(0.75f); JPanel streamingPanelTwitchOAuthPanel = new JPanel(); streamingPanel.add(streamingPanelTwitchOAuthPanel); streamingPanelTwitchOAuthPanel.setLayout( new BoxLayout(streamingPanelTwitchOAuthPanel, BoxLayout.X_AXIS)); - streamingPanelTwitchOAuthPanel.setPreferredSize(new Dimension(0, 37)); + streamingPanelTwitchOAuthPanel.setPreferredSize(osScaleMul(new Dimension(0, twitchTextHeight))); streamingPanelTwitchOAuthPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - streamingPanelTwitchOAuthPanel.setBorder(new EmptyBorder(0, 0, 7, 0)); + streamingPanelTwitchOAuthPanel.setBorder( + BorderFactory.createEmptyBorder(0, 0, osScaleMul(7), 0)); JLabel streamingPanelTwitchOAuthLabel = new JLabel("Your Twitch OAuth token (not your Stream key): "); streamingPanelTwitchOAuthLabel.setToolTipText("Your Twitch OAuth token (not your Stream Key)"); streamingPanelTwitchOAuthPanel.add(streamingPanelTwitchOAuthLabel); - streamingPanelTwitchOAuthLabel.setAlignmentY((float) 0.9); + streamingPanelTwitchOAuthLabel.setAlignmentY(0.9f); streamingPanelTwitchOAuthTextField = new JPasswordField(); streamingPanelTwitchOAuthPanel.add(streamingPanelTwitchOAuthTextField); - streamingPanelTwitchOAuthTextField.setMinimumSize(new Dimension(100, 28)); - streamingPanelTwitchOAuthTextField.setMaximumSize(new Dimension(Short.MAX_VALUE, 28)); - streamingPanelTwitchOAuthTextField.setAlignmentY((float) 0.75); + streamingPanelTwitchOAuthTextField.setMinimumSize(osScaleMul(new Dimension(100, 28))); + streamingPanelTwitchOAuthTextField.setMaximumSize( + new Dimension(Short.MAX_VALUE, osScaleMul(28))); + streamingPanelTwitchOAuthTextField.setAlignmentY(0.75f); streamingPanelSaveLoginCheckbox = addCheckbox("Save login information between logins (Requires restart)", streamingPanel); @@ -1259,6 +1848,8 @@ public void stateChanged(ChangeEvent e) { streamingPanelStartLoginCheckbox.setToolTipText( "Starts the game at the login screen and return to it on logout"); + addPanelBottomGlue(streamingPanel); + /* * Keybind tab */ @@ -1274,6 +1865,7 @@ public void stateChanged(ChangeEvent e) { gbl_constraints.fill = GridBagConstraints.HORIZONTAL; gbl_constraints.anchor = GridBagConstraints.FIRST_LINE_START; gbl_constraints.weightx = 1; + gbl_constraints.ipadx = 20; gbl_constraints.gridy = 0; gbl_constraints.gridwidth = 3; @@ -1281,11 +1873,19 @@ public void stateChanged(ChangeEvent e) { // consider using ALT instead. addKeybindCategory(keybindContainerPanel, "General"); - /* - addKeybindSet(keybindContainerPanel, "Logout", "logout", KeyModifier.CTRL, KeyEvent.VK_L); + // addKeybindSet(keybindContainerPanel, "Logout", "logout", KeyModifier.CTRL, KeyEvent.VK_L); addKeybindSet( - keybindContainerPanel, "Take screenshot", "screenshot", KeyModifier.CTRL, KeyEvent.VK_S); - */ + keybindContainerPanel, "Take screenshot", "screenshot", KeyModifier.CTRL, KeyEvent.VK_S); + addKeybindSet( + keybindContainerPanel, "Toggle scaling", "toggle_scaling", KeyModifier.ALT, KeyEvent.VK_S); + addKeybindSet( + keybindContainerPanel, "Increase scale", "increase_scale", KeyModifier.ALT, KeyEvent.VK_UP); + addKeybindSet( + keybindContainerPanel, + "Decrease scale", + "decrease_scale", + KeyModifier.ALT, + KeyEvent.VK_DOWN); addKeybindSet( keybindContainerPanel, "Show settings window", @@ -1298,6 +1898,12 @@ public void stateChanged(ChangeEvent e) { "show_worldmap_window", KeyModifier.ALT, KeyEvent.VK_M); + addKeybindSet( + keybindContainerPanel, + "Toggle trackpad camera rotation", + "toggle_trackpad_camera_rotation", + KeyModifier.ALT, + KeyEvent.VK_D); /* addKeybindSet( keybindContainerPanel, @@ -1504,7 +2110,7 @@ public void stateChanged(ChangeEvent e) { "Show player controls", "show_player_controls", KeyModifier.ALT, - KeyEvent.VK_UP); + KeyEvent.VK_C); */ @@ -1573,6 +2179,8 @@ public void stateChanged(ChangeEvent e) { keybindContainerContainerPanel.add(keybindContainerPanel, gbl_constraints); keybindPanel.add(keybindContainerContainerPanel, con); + addPanelBottomGlue(keybindPanel); + /* * Presets tab */ @@ -1587,7 +2195,7 @@ public void stateChanged(ChangeEvent e) { JPanel presetsPanelPresetSliderPanel = new JPanel(); presetsPanelPresetSliderPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - presetsPanelPresetSliderPanel.setMaximumSize(new Dimension(400, 175)); + presetsPanelPresetSliderPanel.setMaximumSize(osScaleMul(new Dimension(300, 175))); presetsPanelPresetSliderPanel.setLayout( new BoxLayout(presetsPanelPresetSliderPanel, BoxLayout.X_AXIS)); presetsPanel.add(presetsPanelPresetSliderPanel); @@ -1610,16 +2218,22 @@ public void stateChanged(ChangeEvent e) { presetsPanelPresetSlider.setSnapToTicks(true); presetsPanelPresetSlider.setMinimum(0); presetsPanelPresetSlider.setMaximum(5); - presetsPanelPresetSlider.setPreferredSize(new Dimension(100, 0)); presetsPanelPresetSlider.setAlignmentX(Component.LEFT_ALIGNMENT); - presetsPanelPresetSlider.setBorder(new EmptyBorder(0, 0, 5, 70)); + presetsPanelPresetSlider.setBorder( + BorderFactory.createEmptyBorder(0, 0, osScaleMul(5), osScaleMul(70))); presetsPanelPresetSlider.setOrientation(SwingConstants.VERTICAL); + + if (Util.isUsingFlatLAFTheme()) { + presetsPanelPresetSliderPanel.add(Box.createHorizontalStrut(osScaleMul(35))); + } + presetsPanelPresetSliderPanel.add(presetsPanelPresetSlider); JPanel presetsButtonPanel = new JPanel(); presetsButtonPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - presetsButtonPanel.setMaximumSize(new Dimension(300, 50)); - presetsButtonPanel.setBorder(BorderFactory.createEmptyBorder(7, 10, 10, 0)); + presetsButtonPanel.setMaximumSize(osScaleMul(new Dimension(400, 50))); + presetsButtonPanel.setBorder( + BorderFactory.createEmptyBorder(osScaleMul(7), osScaleMul(10), osScaleMul(10), 0)); presetsButtonPanel.setLayout(new BoxLayout(presetsButtonPanel, BoxLayout.X_AXIS)); replaceConfigButton = @@ -1628,10 +2242,16 @@ public void stateChanged(ChangeEvent e) { new ActionListener() { @Override public void actionPerformed(ActionEvent e) { + String confirmPresetDefaultMessage = + "Warning: this will delete your old settings!
" + + "
" + + "Are you sure you want to delete your old settings?"; + JPanel confirmPresetDefaultPanel = + Util.createOptionMessagePanel(confirmPresetDefaultMessage); int choice = JOptionPane.showConfirmDialog( Launcher.getConfigWindow().frame, - "Warning: this will delete your old settings! Are you sure you want to delete your old settings?", + confirmPresetDefaultPanel, "Confirm", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); @@ -1642,6 +2262,11 @@ public void actionPerformed(ActionEvent e) { Settings.save(Settings.currentProfile); } }); + + if (Util.isUsingFlatLAFTheme()) { + presetsButtonPanel.add(Box.createRigidArea(osScaleMul(new Dimension(4, 0)))); + } + resetPresetsButton = addButton("Reset Presets", presetsButtonPanel, Component.RIGHT_ALIGNMENT); resetPresetsButton.addActionListener( new ActionListener() { @@ -1662,6 +2287,8 @@ public void actionPerformed(ActionEvent e) { } }); + addPanelBottomGlue(presetsPanel); + // World List Tab worldListPanel.setLayout(new BoxLayout(worldListPanel, BoxLayout.Y_AXIS)); worldListPanel.setAlignmentX(Component.LEFT_ALIGNMENT); @@ -1669,7 +2296,7 @@ public void actionPerformed(ActionEvent e) { addSettingsHeader(worldListPanel, "World List"); JLabel spacingLabel = new JLabel(""); - spacingLabel.setBorder(BorderFactory.createEmptyBorder(5, 0, 0, 0)); + spacingLabel.setBorder(BorderFactory.createEmptyBorder(osScaleMul(15), 0, 0, 0)); worldListPanel.add(spacingLabel); for (int i = 1; i <= Settings.WORLDS_TO_DISPLAY; i++) { @@ -1677,6 +2304,8 @@ public void actionPerformed(ActionEvent e) { } addAddWorldButton(); + addPanelBottomGlue(worldListPanel); + // Authors Tab JPanel logoPanel = new JPanel(); @@ -1689,8 +2318,12 @@ public void actionPerformed(ActionEvent e) { try { BufferedImage rsctimesLogo = ImageIO.read(Launcher.getResource("/assets/icon-large.png")); JLabel rsctimesLogoJLabel = - new JLabel(new ImageIcon(rsctimesLogo.getScaledInstance(250, 250, Image.SCALE_DEFAULT))); - rsctimesLogoJLabel.setBorder(BorderFactory.createEmptyBorder(0, 10, 20, 40)); + new JLabel( + new ImageIcon( + rsctimesLogo.getScaledInstance( + osScaleMul(250), osScaleMul(250), Image.SCALE_SMOOTH))); + rsctimesLogoJLabel.setBorder( + BorderFactory.createEmptyBorder(0, osScaleMul(10), osScaleMul(20), osScaleMul(40))); logoPanel.add(rsctimesLogoJLabel); } catch (Exception e) { e.printStackTrace(); @@ -1709,8 +2342,8 @@ public void actionPerformed(ActionEvent e) { JLabel RSCTimesText = new JLabel( String.format( - "
RSCTimes
v%8.6f
", - Settings.VERSION_NUMBER)); + "
RSCTimes
v%8.6f
", + osScaleMul(45), osScaleMul(10), osScaleMul(20), Settings.VERSION_NUMBER)); rightPane.add(RSCTimesText); @@ -1718,14 +2351,20 @@ public void actionPerformed(ActionEvent e) { JLabel aboutText = new JLabel( - "

RSCTimes is a RuneLite-like client " - + "based on mudclient38-recreated.
Learn more at https://rsc.plus

" - + "Thanks to the authors who made this software possible:
" - + "

  • Luis, for creating the client and finding lots of hooks
  • " - + "
  • Logg, helped port features from RSC+, new interfaces & improvements
  • " - + "
  • Ornox, for creating RSC+ & most of its features
  • " - + "
  • The RSC+ team of 2016 to 2018
  • " - + "
  • The Jagex team of 2000 to 2001

"); + String.format( + "" + + "

RSCTimes is a RuneLite-like client " + + "based on mudclient38-recreated.
Learn more at https://rsc.plus

" + + "Thanks to the authors who made this software possible:

    " + + "
  • ● Luis, for creating the client and finding lots of hooks
  • " + + "
  • ● Logg, helped port features from RSC+, new interfaces & improvements
  • " + + "
  • ● conker, ported scaling from RSC+, modern UI, and fixed bugs
  • " + + "
  • ● Yumeko, fixed Twitch chat integration in 2023
  • " + + "
  • ● Ornox, for creating RSC+ & most of its features
  • " + + "
  • ● The RSC+ team of 2016 to 2018
  • " + + "
  • ● The Jagex team of 2000 to 2001
  • " + + "

", + osScaleMul(10), osScaleMul(15))); rightPane.add(aboutText, cR); c.gridx = 2; @@ -1758,6 +2397,44 @@ public void actionPerformed(ActionEvent e) { thirdsPanel.add(bottomPane, c); authorsPanel.add(thirdsPanel); + + addPanelBottomGlue(authorsPanel); + + //// End component creation //// + } + + /** Resets the tooltip listener state */ + private void resetToolTipListener() { + toolTipTextString = " "; + toolTipTextLabel.setText(toolTipInitText); + resetToolTipBarPanelColors(); + removeConfigWindowEventQueueListener(); + } + + /** Resets the tooltip bar panel colors */ + private void resetToolTipBarPanelColors() { + if (Util.isDarkThemeFlatLAF()) { + toolTipPanel.setBackground(new Color(52, 56, 58)); + } else if (Util.isLightThemeFlatLAF()) { + toolTipPanel.setBackground(new Color(235, 235, 235)); + } else { + toolTipPanel.setBackground(new Color(233, 236, 242)); + } + } + + /** + * Adds vertical glue to a settings panel to ensure that components do not shrink / grow when an + * active search removes the scrollbar. This must be the very last component added to a settings + * panel. + * + * @param panel The panel to which glue should be added + */ + private static void addPanelBottomGlue(JPanel panel) { + JComponent panelGlue = (JComponent) Box.createVerticalGlue(); + // These are named such that they can be identified in cases where the panel + // layout dynamically changes, such as during search. + panelGlue.setName(panel.getName().toLowerCase() + "PanelBottomGlue"); + panel.add(panelGlue); } /** @@ -1814,7 +2491,7 @@ private void addKeybindSet( private JLabel addKeybindLabel(JPanel panel, String labelText) { GridBagConstraints gbc = new GridBagConstraints(); gbc.anchor = GridBagConstraints.WEST; - gbc.insets = new Insets(0, 0, 5, 0); + gbc.insets = new Insets(0, 0, osScaleMul(5), 0); gbc.gridx = 0; gbc.gridy = keybindLabelGridYCounter++; gbc.weightx = 20; @@ -1841,7 +2518,7 @@ private JButton addKeybindButton(JPanel panel, String buttonText) { GridBagConstraints gbc = new GridBagConstraints(); gbc.anchor = GridBagConstraints.EAST; gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.insets = new Insets(0, 0, 5, 0); + gbc.insets = new Insets(0, 0, osScaleMul(5), 0); gbc.gridx = 1; gbc.gridy = keybindButtonGridYCounter++; JButton jbtn = new JButton(buttonText); @@ -1877,10 +2554,12 @@ private void addKeybindCategorySeparator(JPanel panel) { keybindLabelGridYCounter++; gbc.gridwidth = 2; - panel.add(Box.createVerticalStrut(7), gbc); + JComponent spacer1 = (JComponent) Box.createVerticalStrut(osScaleMul(7)); + panel.add(spacer1, gbc); JSeparator jsep = new JSeparator(SwingConstants.HORIZONTAL); panel.add(jsep, gbc); - panel.add(Box.createVerticalStrut(7), gbc); + JComponent spacer2 = (JComponent) Box.createVerticalStrut(osScaleMul(7)); + panel.add(spacer2, gbc); } /** @@ -1895,7 +2574,7 @@ private JLabel addKeybindCategoryLabel(JPanel panel, String categoryName) { gbc.anchor = GridBagConstraints.WEST; gbc.gridx = 0; if (keybindLabelGridYCounter == 0) gbc.insets = new Insets(0, 0, 0, 0); - else gbc.insets = new Insets(7, 0, 0, 0); + else gbc.insets = new Insets(osScaleMul(7), 0, 0, 0); gbc.gridy = keybindLabelGridYCounter++; keybindButtonGridYCounter++; gbc.weightx = 20; @@ -1924,7 +2603,8 @@ private void addSettingsHeader(JPanel panel, String categoryName) { */ private void addSettingsHeaderSeparator(JPanel panel) { JSeparator jsep = new JSeparator(SwingConstants.HORIZONTAL); - jsep.setMaximumSize(new Dimension(Short.MAX_VALUE, 7)); + jsep.setPreferredSize(new Dimension(0, osScaleMul(7))); + jsep.setMaximumSize(new Dimension(Short.MAX_VALUE, osScaleMul(7))); panel.add(jsep); } @@ -1952,7 +2632,7 @@ private JLabel addSettingsHeaderLabel(JPanel panel, String categoryName) { private JCheckBox addCheckbox(String text, Container container) { JCheckBox checkbox = new JCheckBox(text); checkbox.setAlignmentX(Component.LEFT_ALIGNMENT); - checkbox.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 5)); + checkbox.setBorder(BorderFactory.createEmptyBorder(0, 0, osScaleMul(10), osScaleMul(5))); container.add(checkbox); return checkbox; } @@ -1986,7 +2666,8 @@ private JButton addButton(String text, Container container, float alignment) { private JRadioButton addRadioButton(String text, Container container, int leftIndent) { JRadioButton radioButton = new JRadioButton(text); radioButton.setAlignmentX(Component.LEFT_ALIGNMENT); - radioButton.setBorder(BorderFactory.createEmptyBorder(0, leftIndent, 7, 5)); + radioButton.setBorder( + BorderFactory.createEmptyBorder(0, leftIndent, osScaleMul(7), osScaleMul(5))); container.add(radioButton); return radioButton; } @@ -2025,6 +2706,24 @@ public void synchronizeGuiValues() { Settings.CUSTOM_CLIENT_SIZE_X.get(Settings.currentProfile)); generalPanelClientSizeYSpinner.setValue( Settings.CUSTOM_CLIENT_SIZE_Y.get(Settings.currentProfile)); + generalPanelScaleWindowCheckbox.setSelected( + Settings.SCALED_CLIENT_WINDOW.get(Settings.currentProfile)); + if (Settings.SCALING_ALGORITHM.get(Settings.currentProfile) + == AffineTransformOp.TYPE_NEAREST_NEIGHBOR) { + generalPanelIntegerScalingFocusButton.setSelected(true); + } else if (Settings.SCALING_ALGORITHM.get(Settings.currentProfile) + == AffineTransformOp.TYPE_BILINEAR) { + generalPanelBilinearScalingFocusButton.setSelected(true); + } else if (Settings.SCALING_ALGORITHM.get(Settings.currentProfile) + == AffineTransformOp.TYPE_BICUBIC) { + generalPanelBicubicScalingFocusButton.setSelected(true); + } + generalPanelIntegerScalingSpinner.setValue( + Settings.INTEGER_SCALING_FACTOR.get(Settings.currentProfile)); + generalPanelBilinearScalingSpinner.setValue( + Settings.BILINEAR_SCALING_FACTOR.get(Settings.currentProfile)); + generalPanelBicubicScalingSpinner.setValue( + Settings.BICUBIC_SCALING_FACTOR.get(Settings.currentProfile)); generalPanelCheckUpdates.setSelected(Settings.CHECK_UPDATES.get(Settings.currentProfile)); generalPanelWelcomeEnabled.setSelected( Settings.REMIND_HOW_TO_OPEN_SETTINGS.get(Settings.currentProfile)); @@ -2075,10 +2774,18 @@ public void synchronizeGuiValues() { Settings.AUTO_SCREENSHOT.get(Settings.currentProfile)); generalPanelCustomCursorCheckbox.setSelected( Settings.SOFTWARE_CURSOR.get(Settings.currentProfile)); + generalPanelShiftScrollCameraRotationCheckbox.setSelected( + Settings.SHIFT_SCROLL_CAMERA_ROTATION.get(Settings.currentProfile)); + generalPanelTrackpadRotationSlider.setValue( + Settings.TRACKPAD_ROTATION_SENSITIVITY.get(Settings.currentProfile)); generalPanelViewDistanceSlider.setValue(Settings.VIEW_DISTANCE.get(Settings.currentProfile)); generalPanelPatchGenderCheckbox.setSelected(Settings.PATCH_GENDER.get(Settings.currentProfile)); generalPanelPatchHbar512LastPixelCheckbox.setSelected( Settings.PATCH_HBAR_512_LAST_PIXEL.get(Settings.currentProfile)); + generalPanelUseDarkModeCheckbox.setSelected( + Settings.USE_DARK_FLATLAF.get(Settings.currentProfile)); + generalPanelUseNimbusThemeCheckbox.setSelected( + Settings.USE_NIMBUS_THEME.get(Settings.currentProfile)); generalPanelPrefersXdgOpenCheckbox.setSelected( Settings.PREFERS_XDG_OPEN.get(Settings.currentProfile)); @@ -2174,8 +2881,6 @@ public void synchronizeGuiValues() { Settings.TRADE_NOTIFICATIONS.get(Settings.currentProfile)); notificationPanelUnderAttackNotifsCheckbox.setSelected( Settings.UNDER_ATTACK_NOTIFICATIONS.get(Settings.currentProfile)); - notificationPanelLogoutNotifsCheckbox.setSelected( - Settings.LOGOUT_NOTIFICATIONS.get(Settings.currentProfile)); notificationPanelLowHPNotifsCheckbox.setSelected( Settings.LOW_HP_NOTIFICATIONS.get(Settings.currentProfile)); notificationPanelLowHPNotifsSpinner.setValue( @@ -2239,6 +2944,36 @@ public void saveSettings() { Settings.CUSTOM_CLIENT_SIZE_Y.put( Settings.currentProfile, ((SpinnerNumberModel) (generalPanelClientSizeYSpinner.getModel())).getNumber().intValue()); + Settings.SCALED_CLIENT_WINDOW.put( + Settings.currentProfile, generalPanelScaleWindowCheckbox.isSelected()); + Settings.SCALING_ALGORITHM.put( + Settings.currentProfile, + generalPanelIntegerScalingFocusButton.isSelected() + ? AffineTransformOp.TYPE_NEAREST_NEIGHBOR + : generalPanelBilinearScalingFocusButton.isSelected() + ? AffineTransformOp.TYPE_BILINEAR + : AffineTransformOp.TYPE_BICUBIC); + Settings.INTEGER_SCALING_FACTOR.put( + Settings.currentProfile, + ((SpinnerNumberModel) (generalPanelIntegerScalingSpinner.getModel())) + .getNumber() + .intValue()); + Settings.BILINEAR_SCALING_FACTOR.put( + Settings.currentProfile, + BigDecimal.valueOf( + ((SpinnerNumberModel) (generalPanelBilinearScalingSpinner.getModel())) + .getNumber() + .floatValue()) + .setScale(1, RoundingMode.HALF_DOWN) + .floatValue()); + Settings.BICUBIC_SCALING_FACTOR.put( + Settings.currentProfile, + BigDecimal.valueOf( + ((SpinnerNumberModel) (generalPanelBicubicScalingSpinner.getModel())) + .getNumber() + .floatValue()) + .setScale(1, RoundingMode.HALF_DOWN) + .floatValue()); Settings.CHECK_UPDATES.put(Settings.currentProfile, generalPanelCheckUpdates.isSelected()); Settings.REMIND_HOW_TO_OPEN_SETTINGS.put( Settings.currentProfile, generalPanelWelcomeEnabled.isSelected()); @@ -2292,6 +3027,10 @@ public void saveSettings() { Settings.FOV.put(Settings.currentProfile, generalPanelFoVSlider.getValue()); Settings.SOFTWARE_CURSOR.put( Settings.currentProfile, generalPanelCustomCursorCheckbox.isSelected()); + Settings.SHIFT_SCROLL_CAMERA_ROTATION.put( + Settings.currentProfile, generalPanelShiftScrollCameraRotationCheckbox.isSelected()); + Settings.TRACKPAD_ROTATION_SENSITIVITY.put( + Settings.currentProfile, generalPanelTrackpadRotationSlider.getValue()); Settings.AUTO_SCREENSHOT.put( Settings.currentProfile, generalPanelAutoScreenshotCheckbox.isSelected()); Settings.VIEW_DISTANCE.put(Settings.currentProfile, generalPanelViewDistanceSlider.getValue()); @@ -2304,6 +3043,10 @@ public void saveSettings() { Settings.currentProfile, generalPanelPatchGenderCheckbox.isSelected()); Settings.PATCH_HBAR_512_LAST_PIXEL.put( Settings.currentProfile, generalPanelPatchHbar512LastPixelCheckbox.isSelected()); + Settings.USE_DARK_FLATLAF.put( + Settings.currentProfile, generalPanelUseDarkModeCheckbox.isSelected()); + Settings.USE_NIMBUS_THEME.put( + Settings.currentProfile, generalPanelUseNimbusThemeCheckbox.isSelected()); // Overlays options Settings.SHOW_HP_OVERLAY.put( @@ -2371,8 +3114,6 @@ public void saveSettings() { Settings.currentProfile, notificationPanelTradeNotifsCheckbox.isSelected()); Settings.UNDER_ATTACK_NOTIFICATIONS.put( Settings.currentProfile, notificationPanelUnderAttackNotifsCheckbox.isSelected()); - Settings.LOGOUT_NOTIFICATIONS.put( - Settings.currentProfile, notificationPanelLogoutNotifsCheckbox.isSelected()); Settings.LOW_HP_NOTIFICATIONS.put( Settings.currentProfile, notificationPanelLowHPNotifsCheckbox.isSelected()); Settings.LOW_HP_NOTIF_VALUE.put( @@ -2504,11 +3245,11 @@ public static void setKeybindButtonText(KeybindSet kbs) { */ public void applySettings() { saveSettings(); - if (Settings.CUSTOM_CLIENT_SIZE.get(Settings.currentProfile)) - Game.getInstance().resizeFrameWithContents(); + // Tell the Renderer to update the scale from its thread to avoid thread-safety issues. + Settings.renderingScalarUpdateRequired = true; // Tell the Renderer to update the FoV from its thread to avoid thread-safety issues. Settings.fovUpdateRequired = true; - Settings.checkSoftwareCursor(true); + Settings.checkSoftwareCursor(); Camera.setDistance(Settings.VIEW_DISTANCE.get(Settings.currentProfile)); synchronizeGuiValues(); // QueueWindow.syncColumnsWithSettings(); @@ -2545,17 +3286,22 @@ public void addWorldFields(int i) { cR.gridwidth = 1; JLabel worldNumberJLabel = new JLabel(String.format("World %d", i)); - worldNumberJLabel.setAlignmentY((float) 0.75); + worldNumberJLabel.setAlignmentY(0.75f); worldListTitleTextFieldContainers.get(i).add(worldNumberJLabel, cR); cR.weightx = 0.5; cR.gridwidth = 5; + if (Util.isUsingFlatLAFTheme()) { + cR.insets = new Insets(0, 0, 0, osScaleMul(4)); + } + worldNamesJTextFields.put(i, new HintTextField("Name of World")); - worldNamesJTextFields.get(i).setMinimumSize(new Dimension(80, 28)); - worldNamesJTextFields.get(i).setMaximumSize(new Dimension(300, 28)); - worldNamesJTextFields.get(i).setPreferredSize(new Dimension(200, 28)); - worldNamesJTextFields.get(i).setAlignmentY((float) 0.75); + worldNamesJTextFields.get(i).setMinimumSize(osScaleMul(new Dimension(80, 28))); + worldNamesJTextFields.get(i).setMaximumSize(osScaleMul(new Dimension(300, 28))); + worldNamesJTextFields.get(i).setPreferredSize(osScaleMul(new Dimension(202, 28))); + worldNamesJTextFields.get(i).setAlignmentY(0.75f); + worldListTitleTextFieldContainers.get(i).add(worldNamesJTextFields.get(i), cR); cR.weightx = 0.1; @@ -2564,16 +3310,20 @@ public void addWorldFields(int i) { /* JLabel spacingJLabel = new JLabel(""); - worldNumberJLabel.setAlignmentY((float) 0.75); + worldNumberJLabel.setAlignmentY(0.75f); worldListTitleTextFieldContainers.get(i).add(spacingJLabel, cR); */ cR.weightx = 0.3; cR.gridwidth = 1; + if (Util.isUsingFlatLAFTheme()) { + cR.insets = new Insets(0, 0, 0, 0); + } + worldDeleteJButtons.put(i, new JButton("Delete World")); - worldDeleteJButtons.get(i).setAlignmentY((float) 0.80); - worldDeleteJButtons.get(i).setPreferredSize(new Dimension(50, 28)); + worldDeleteJButtons.get(i).setAlignmentY(0.80f); + worldDeleteJButtons.get(i).setPreferredSize(osScaleMul(new Dimension(50, 28))); worldDeleteJButtons.get(i).setActionCommand(String.format("%d", i)); worldDeleteJButtons .get(i) @@ -2582,12 +3332,15 @@ public void addWorldFields(int i) { @Override public void actionPerformed(ActionEvent e) { String actionCommandWorld = e.getActionCommand(); + + JPanel confirmDeleteWorldPanel = + Util.createOptionMessagePanel( + "Warning: Are you sure you want to DELETE World %s?", + actionCommandWorld); int choice = JOptionPane.showConfirmDialog( Launcher.getConfigWindow().frame, - String.format( - "Warning: Are you sure you want to DELETE World %s?", - actionCommandWorld), + confirmDeleteWorldPanel, "Confirm", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); @@ -2602,7 +3355,7 @@ public void actionPerformed(ActionEvent e) { worldListTitleTextFieldContainers.get(i).add(worldDeleteJButtons.get(i), cR); - worldListTitleTextFieldContainers.get(i).setMaximumSize(new Dimension(680, 40)); + worldListTitleTextFieldContainers.get(i).setMaximumSize(osScaleMul(new Dimension(680, 40))); worldListPanel.add(worldListTitleTextFieldContainers.get(i)); //// URL/Ports line @@ -2610,14 +3363,15 @@ public void actionPerformed(ActionEvent e) { worldPortsJTextFields.put( i, new HintTextField(String.format("World %d Port (default: 43594)", i))); - worldUrlsJTextFields.get(i).setMinimumSize(new Dimension(100, 28)); - worldUrlsJTextFields.get(i).setMaximumSize(new Dimension(500, 28)); - worldUrlsJTextFields.get(i).setPreferredSize(new Dimension(500, 28)); - worldUrlsJTextFields.get(i).setAlignmentY((float) 0.75); + worldUrlsJTextFields.get(i).setMinimumSize(osScaleMul(new Dimension(100, 28))); + worldUrlsJTextFields.get(i).setMaximumSize(osScaleMul(new Dimension(500, 28))); + worldUrlsJTextFields.get(i).setPreferredSize(osScaleMul(new Dimension(500, 28))); + worldUrlsJTextFields.get(i).setAlignmentY(0.75f); - worldPortsJTextFields.get(i).setMinimumSize(new Dimension(100, 28)); - worldPortsJTextFields.get(i).setMaximumSize(new Dimension(180, 28)); - worldPortsJTextFields.get(i).setAlignmentY((float) 0.75); + int portOffset = Util.isUsingFlatLAFTheme() ? 4 : 0; + worldPortsJTextFields.get(i).setMinimumSize(osScaleMul(new Dimension(100 - portOffset, 28))); + worldPortsJTextFields.get(i).setMaximumSize(osScaleMul(new Dimension(180 - portOffset, 28))); + worldPortsJTextFields.get(i).setAlignmentY(0.75f); worldListURLPortTextFieldContainers.put(i, new JPanel()); @@ -2626,11 +3380,21 @@ public void actionPerformed(ActionEvent e) { .setLayout(new BoxLayout(worldListURLPortTextFieldContainers.get(i), BoxLayout.X_AXIS)); worldListURLPortTextFieldContainers.get(i).add(worldUrlsJTextFields.get(i)); + if (Util.isUsingFlatLAFTheme()) { + JLabel spacingLabel = new JLabel(""); + spacingLabel.setBorder(BorderFactory.createEmptyBorder(0, osScaleMul(4), 0, 0)); + worldListURLPortTextFieldContainers.get(i).add(spacingLabel); + worldListURLPortTextFieldContainers + .get(i) + .setBorder(BorderFactory.createEmptyBorder(osScaleMul(4), 0, osScaleMul(4), 0)); + } worldListURLPortTextFieldContainers.get(i).add(worldPortsJTextFields.get(i)); worldListPanel.add(worldListURLPortTextFieldContainers.get(i)); worldListSpacingLabels.put(i, new JLabel("")); - worldListSpacingLabels.get(i).setBorder(BorderFactory.createEmptyBorder(30, 0, 0, 0)); + worldListSpacingLabels + .get(i) + .setBorder(BorderFactory.createEmptyBorder(osScaleMul(30), 0, 0, 0)); worldListPanel.add(worldListSpacingLabels.get(i)); if (i > Settings.WORLD_NAMES.size()) { @@ -2640,14 +3404,27 @@ public void actionPerformed(ActionEvent e) { public void addAddWorldButton() { JButton addWorldButton = new JButton("Add New World"); + addWorldButton.setAlignmentX(JButton.CENTER_ALIGNMENT); addWorldButton.addActionListener( new ActionListener() { @Override public void actionPerformed(ActionEvent e) { worldListPanel.remove(addWorldButton); + Component verticalGlue = + Arrays.stream(worldListPanel.getComponents()) + .filter( + c -> c.getName() != null && c.getName().equals("world_listPanelBottomGlue")) + .findFirst() + .orElse(null); + if (verticalGlue != null) { + worldListPanel.remove(verticalGlue); + } ++Settings.WORLDS_TO_DISPLAY; synchronizeWorldTab(); addAddWorldButton(); + if (verticalGlue != null) { + worldListPanel.add(verticalGlue); + } } }); worldListPanel.add(addWorldButton); @@ -2655,6 +3432,88 @@ public void actionPerformed(ActionEvent e) { worldListPanel.repaint(); } + public void updateCustomClientSizeMinValues(Dimension updatedMinimumWindowSize) { + spinnerWinXModel.setMinimum(updatedMinimumWindowSize.width); + spinnerWinYModel.setMinimum(updatedMinimumWindowSize.height); + } + + /** + * Creates an EventQueue listener, used to capture {@link MouseEvent#MOUSE_ENTERED} events for + * display tooltip text within the {@link #toolTipPanel} + * + * @return The constructed {@link AWTEventListener} instance + */ + private AWTEventListener createConfigWindowEventQueueListener() { + return new AWTEventListener() { + @Override + public void eventDispatched(AWTEvent e) { + try { + // Exit early if the label hasn't been initialized + if (toolTipTextLabel == null) { + return; + } + + // Exit early for things that aren't MOUSE_ENTERED events + if (e.getID() != MouseEvent.MOUSE_ENTERED) { + return; + } + + // Exit early for events that aren't on a JComponent + if (!(e.getSource() instanceof JComponent)) { + return; + } + + // Exit early for events that did not originate from the ConfigWindow + if (SwingUtilities.getWindowAncestor((JComponent) e.getSource()) != frame) { + return; + } + + String componentToolTipText = ((JComponent) e.getSource()).getToolTipText(); + + if (componentToolTipText != null && !componentToolTipText.equals(toolTipTextString)) { + toolTipTextString = componentToolTipText; + toolTipTextLabel.setText(toolTipTextString); + } + } catch (Exception ex) { + Logger.Error( + "There was an error with processing the MOUSE_ENTERED event listener." + + "Please screenshot and report this error if possible."); + ex.printStackTrace(); + } + } + }; + } + + /** Attaches the EventQueue listener */ + private void addConfigWindowEventQueueListener() { + if (isListeningForEventQueue) { + return; + } + + // Disable tooltips + ToolTipManager.sharedInstance().setEnabled(false); + + // Add listener + Toolkit.getDefaultToolkit().addAWTEventListener(eventQueueListener, AWTEvent.MOUSE_EVENT_MASK); + + isListeningForEventQueue = true; + } + + /** Detaches the EventQueue listener */ + private void removeConfigWindowEventQueueListener() { + if (!isListeningForEventQueue) { + return; + } + + // Enable tooltips + ToolTipManager.sharedInstance().setEnabled(true); + + // Remove listener + Toolkit.getDefaultToolkit().removeAWTEventListener(eventQueueListener); + + isListeningForEventQueue = false; + } + // adds or removes world list text fields & fills them with their values public void synchronizeWorldTab() { int numberOfWorldsEver = worldUrlsJTextFields.size(); diff --git a/src/Client/FlatLaf/FlatDarkLaf.properties b/src/Client/FlatLaf/FlatDarkLaf.properties new file mode 100644 index 0000000..b3592b6 --- /dev/null +++ b/src/Client/FlatLaf/FlatDarkLaf.properties @@ -0,0 +1,9 @@ +@background=#2a2e30 +@foreground=#dddddd + +TabbedPane.background=#34383a +TabbedPane.selectedBackground=#222222 + +Table.alternateRowColor=#1e4646 + +Separator.foreground=mix(@accentUnderlineColor,$TabbedPane.background,60%) diff --git a/src/Client/FlatLaf/FlatLightLaf.properties b/src/Client/FlatLaf/FlatLightLaf.properties new file mode 100644 index 0000000..bcfd426 --- /dev/null +++ b/src/Client/FlatLaf/FlatLightLaf.properties @@ -0,0 +1,6 @@ +TabbedPane.background=#ebebeb +TabbedPane.selectedBackground=#ffffff + +Table.alternateRowColor=#bedcf0 + +Separator.foreground=mix(@accentUnderlineColor,$TabbedPane.background,50%) diff --git a/src/Client/JClassPatcher.java b/src/Client/JClassPatcher.java index 8a9ea74..c0a0d5e 100644 --- a/src/Client/JClassPatcher.java +++ b/src/Client/JClassPatcher.java @@ -551,6 +551,37 @@ private void patchApplet(ClassNode node) { } } } + + // draw loading screen + if (methodNode.name.equals("hj") && methodNode.desc.equals("(ILjava/lang/String;)V")) { + Iterator insnNodeList = methodNode.instructions.iterator(); + while (insnNodeList.hasNext()) { + AbstractInsnNode insnNode = insnNodeList.next(); + + if (insnNode.getOpcode() == Opcodes.INVOKEVIRTUAL + && ((MethodInsnNode) insnNode).name.equals("setColor")) { + + insnNode = insnNode.getPrevious().getPrevious().getPrevious(); + + methodNode.instructions.insertBefore(insnNode, new VarInsnNode(Opcodes.ALOAD, 0)); + + methodNode.instructions.insertBefore( + insnNode, + new MethodInsnNode( + Opcodes.INVOKESTATIC, + "Client/ScaledWindow", + "hookLoadingGraphics", + "()Ljava/awt/Graphics;", + false)); + + methodNode.instructions.insertBefore( + insnNode, + new FieldInsnNode(Opcodes.PUTFIELD, "jagex/client/k", "cq", "Ljava/awt/Graphics;")); + + break; + } + } + } } } @@ -1816,16 +1847,11 @@ private void patchRenderer(ClassNode node) { methodNode.instructions.insert( findNode, new MethodInsnNode( - Opcodes.INVOKESTATIC, - "Game/Renderer", - "present", - "(Ljava/awt/Graphics;Ljava/awt/Image;)V", - false)); + Opcodes.INVOKESTATIC, "Game/Renderer", "present", "(Ljava/awt/Image;)V", false)); methodNode.instructions.insert( findNode, new FieldInsnNode(Opcodes.GETFIELD, node.name, imageNode.name, imageNode.desc)); methodNode.instructions.insert(findNode, new VarInsnNode(Opcodes.ALOAD, 0)); - methodNode.instructions.insert(findNode, new VarInsnNode(Opcodes.ALOAD, 1)); } // Drawstring ~1000~ fix if (methodNode.name.equals("ef") && methodNode.desc.equals("(Ljava/lang/String;IIII)V")) { diff --git a/src/Client/Launcher.java b/src/Client/Launcher.java index 184c79b..108d134 100644 --- a/src/Client/Launcher.java +++ b/src/Client/Launcher.java @@ -18,22 +18,32 @@ */ package Client; +import static Client.Util.osScaleMul; + import Game.Client; import Game.Game; +import com.sun.jna.platform.win32.GDI32; +import com.sun.jna.platform.win32.WinDef; import java.applet.Applet; import java.awt.AWTEvent; import java.awt.Color; import java.awt.Dimension; +import java.awt.Font; +import java.awt.Image; import java.awt.Toolkit; import java.awt.event.AWTEventListener; import java.awt.event.KeyEvent; +import java.awt.image.BufferedImage; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLConnection; +import java.nio.file.Files; import java.util.Properties; +import javax.imageio.ImageIO; import javax.swing.*; /** Singleton main class which renders a loading window and the game client window. */ @@ -41,15 +51,23 @@ public class Launcher extends JFrame implements Runnable { // Singleton private static Launcher instance; + private static ScaledWindow scaledWindow; private static ConfigWindow configWindow; private static WorldMapWindow worldMapWindow; public static ImageIcon icon = null; + public static ImageIcon scaled_option_icon = null; public static ImageIcon icon_warn = null; + public static ImageIcon scaled_icon_warn = null; private JProgressBar m_progressBar; private JClassLoader m_classLoader; + public static double OSScalingFactor = 1.0; + public static boolean forceDisableNimbus = false; + + public static int numCores; + private Launcher() { // Empty private constructor to prevent extra instances from being created. } @@ -57,21 +75,52 @@ private Launcher() { /** Renders the launcher progress bar window, then calls {@link #run()}. */ public void init() { Logger.start(); - Logger.Info("Starting rsctimes"); + Logger.Info("Starting RSCTimes"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); getContentPane().setBackground(Color.BLACK); // Set window icon - URL iconURL = getResource("/assets/RSCX.logo.png"); + URL iconURL = getResource("/assets/icon.png"); if (iconURL != null) { icon = new ImageIcon(iconURL); setIconImage(icon.getImage()); + + // Set scaled icon used in JOptionPanes + try { + BufferedImage iconOptionsBI = ImageIO.read(iconURL); + scaled_option_icon = + new ImageIcon( + iconOptionsBI.getScaledInstance( + osScaleMul(iconOptionsBI.getWidth()), + osScaleMul(iconOptionsBI.getHeight()), + Image.SCALE_DEFAULT)); + } catch (IOException e) { + // No-op + } + } + + iconURL = getResource("/assets/icon_warn.png"); + if (iconURL != null) { + icon_warn = new ImageIcon(iconURL); + + // Set scaled warning icon for JOptionPanes + try { + BufferedImage warnIconBI = ImageIO.read(iconURL); + scaled_icon_warn = + new ImageIcon( + warnIconBI.getScaledInstance( + osScaleMul(warnIconBI.getWidth()), + osScaleMul(warnIconBI.getHeight()), + Image.SCALE_DEFAULT)); + } catch (IOException e) { + // No-op + } } // Set size - getContentPane().setPreferredSize(new Dimension(280, 32)); - setTitle("rsctimes Launcher"); + getContentPane().setPreferredSize(osScaleMul(new Dimension(280, 32))); + setTitle("RSCTimes Launcher"); setResizable(false); pack(); setLocationRelativeTo(null); @@ -80,12 +129,44 @@ public void init() { m_progressBar = new JProgressBar(); m_progressBar.setStringPainted(true); m_progressBar.setBorderPainted(true); - m_progressBar.setForeground(Color.GRAY.brighter()); + m_progressBar.setForeground(Color.GRAY); m_progressBar.setBackground(Color.BLACK); + if (Util.isUsingFlatLAFTheme()) { + m_progressBar.setFont(new Font(Font.SERIF, Font.PLAIN, osScaleMul(14))); + } m_progressBar.setString("Initializing"); getContentPane().add(m_progressBar); setVisible(true); + + // Check to see if RSCx has permissions to write to the current dir + try { + if (!Files.isWritable(new File(Settings.Dir.JAR).toPath())) { + String filePermissionsErrorMessage = + "Error attempting to launch RSCTimes
" + + "
" + + "RSCTimes is unable to create necessary files in the current directory.
" + + "
" + + "You must either grant the appropriate permissions to this directory OR
" + + "move the application to a different location."; + JPanel filePermissionsErrorPanel = + Util.createOptionMessagePanel(filePermissionsErrorMessage); + + JOptionPane.showConfirmDialog( + this, + filePermissionsErrorPanel, + "RSCTimes", + JOptionPane.DEFAULT_OPTION, + JOptionPane.ERROR_MESSAGE, + scaled_icon_warn); + + System.exit(0); + } + } catch (Exception e) { + Logger.Error("There was an error on startup checking for directory permissions."); + e.printStackTrace(); + } + new Thread(this).start(); } @@ -94,40 +175,53 @@ public void init() { public void run() { if (Settings.UPDATE_CONFIRMATION.get(Settings.currentProfile)) { Client.firstTimeRunningRSCTimes = true; + String automaticUpdateMessage = + "RSCTimes has an automatic update feature.
" + + "
" + + "When enabled, RSCTimes will prompt for and install updates when launching the client.
" + + "The updates are obtained from our 'Latest' release on GitHub.
" + + "
" + + "Would you like to enable this feature?
" + + "
" + + "NOTE: This option can be toggled in the Settings interface under the General tab."; + + JPanel automaticUpdatePanel = Util.createOptionMessagePanel(automaticUpdateMessage); + int response = JOptionPane.showConfirmDialog( this, - "rsctimes has an automatic update feature.\n" - + "\n" - + "When enabled, rsctimes will prompt for and install updates when launching the client.\n" - + "The updates are obtained from our 'Latest' release on GitHub.\n" - + "\n" - + "Would you like to enable this feature?\n" - + "\n" - + "NOTE: This option can be toggled in the Settings interface under the General tab.", - "rsctimes", + automaticUpdatePanel, + "RSCTimes", JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE, - icon); + scaled_option_icon); if (response == JOptionPane.YES_OPTION || response == JOptionPane.CLOSED_OPTION) { Settings.CHECK_UPDATES.put(Settings.currentProfile, true); + + JPanel updateInfoPanel = + Util.createOptionMessagePanel( + "RSCTimes is set to check for updates on GitHub at every launch!"); + JOptionPane.showMessageDialog( - this, - "rsctimes is set to check for updates on GitHub at every launch!", - "rsctimes", - JOptionPane.INFORMATION_MESSAGE, - icon); + this, updateInfoPanel, "RSCTimes", JOptionPane.INFORMATION_MESSAGE, scaled_option_icon); } else if (response == JOptionPane.NO_OPTION) { Settings.CHECK_UPDATES.put(Settings.currentProfile, false); + + String automaticUpdateDeniedMessage = + "RSCTimes will not check for updates automatically.
" + + "
" + + "You will not get notified when new releases are available. To update your client, you
" + + "will need to do it manually by replacing 'rsctimes.jar' in your RSCTimes directory.
" + + "
" + + "You can enable GitHub updates again in the Settings interface under the General tab."; + + JPanel automaticUpdateDeniedPanel = + Util.createOptionMessagePanel(automaticUpdateDeniedMessage); + JOptionPane.showMessageDialog( this, - "rsctimes will not check for updates automatically.\n" - + "\n" - + "You will not get notified when new releases are available. To update your client, you\n" - + "will need to do it manually by replacing 'rsctimes.jar' in your rsctimes directory.\n" - + "\n" - + "You can enable GitHub updates again in the Settings interface under the General tab.", - "rsctimes", + automaticUpdateDeniedPanel, + "RSCTimes", JOptionPane.INFORMATION_MESSAGE, icon_warn); } @@ -136,50 +230,60 @@ public void run() { } if (Settings.CHECK_UPDATES.get(Settings.currentProfile)) { - setStatus("Checking for rsctimes update..."); + setStatus("Checking for RSCTimes update..."); double latestVersion = Client.fetchLatestVersionNumber(); if (Settings.VERSION_NUMBER < latestVersion) { - setStatus("rsctimes update is available"); + setStatus("RSCTimes update is available"); // TODO: before Y10K update this to %9.6f + + String clientUpdateMessage = + "An RSCTimes client update is available!
" + + "
" + + "Latest: " + + String.format("%8.6f", latestVersion) + + "
" + + "Installed: " + + String.format("%8.6f", Settings.VERSION_NUMBER) + + "
" + + "
" + + "Would you like to update now?"; + JPanel clientUpdatePanel = Util.createOptionMessagePanel(clientUpdateMessage); int response = JOptionPane.showConfirmDialog( this, - "An rsctimes client update is available!\n" - + "\n" - + "Latest: " - + String.format("%8.6f", latestVersion) - + "\n" - + "Installed: " - + String.format("%8.6f", Settings.VERSION_NUMBER) - + "\n" - + "\n" - + "Would you like to update now?", - "rsctimes", + clientUpdatePanel, + "RSCTimes", JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE, - icon); + scaled_option_icon); if (response == JOptionPane.YES_OPTION) { if (updateJar()) { + String updateSuccessMessage = + "RSCTimes has been updated successfully!
" + + "
" + + "The client requires a restart, and will now exit."; + JPanel updateSuccessPanel = Util.createOptionMessagePanel(updateSuccessMessage); JOptionPane.showMessageDialog( this, - "rsctimes has been updated successfully!\n" - + "\n" - + "The client requires a restart, and will now exit.", - "rsctimes", + updateSuccessPanel, + "RSCTimes", JOptionPane.INFORMATION_MESSAGE, - icon); + scaled_option_icon); System.exit(0); } else { + String updateFailureMessage = + "RSCTimes has failed to update, please try again later.
" + + "
" + + "Would you like to continue without updating?"; + JPanel updateFailurePanel = Util.createOptionMessagePanel(updateFailureMessage); response = JOptionPane.showConfirmDialog( this, - "rsctimes has failed to update, please try again later.\n" - + "\n" - + "Would you like to continue without updating?", - "rsctimes", + updateFailurePanel, + "RSCTimes", JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE, - icon_warn); + scaled_icon_warn); if (response == JOptionPane.NO_OPTION || response == JOptionPane.CLOSED_OPTION) { System.exit(0); } @@ -236,7 +340,7 @@ public void eventDispatched(AWTEvent event) { public boolean updateJar() { boolean success = true; - setStatus("Starting rsctimes update..."); + setStatus("Starting RSCTimes update..."); setProgress(0, 1); try { @@ -257,7 +361,7 @@ public boolean updateJar() { int readSize; while ((readSize = input.read(data, offset, size - offset)) != -1) { offset += readSize; - setStatus("Updating rsctimes (" + (offset / 1024) + "KiB / " + (size / 1024) + "KiB)"); + setStatus("Updating RSCTimes (" + (offset / 1024) + "KiB / " + (size / 1024) + "KiB)"); setProgress(offset, size); } @@ -270,7 +374,7 @@ public boolean updateJar() { output.write(data); output.close(); - setStatus("rsctimes update complete"); + setStatus("RSCTimes update complete"); } } catch (Exception e) { success = false; @@ -338,7 +442,59 @@ public JClassLoader getClassLoader() { return m_classLoader; } + /* Uses JNA to acquire accurate scale factor for JRE 8 */ + public static double getScaleFactor() { + WinDef.HDC hdc = GDI32.INSTANCE.CreateCompatibleDC(null); + if (hdc != null) { + float actual = GDI32.INSTANCE.GetDeviceCaps(hdc, 10); + float logical = GDI32.INSTANCE.GetDeviceCaps(hdc, 117); + GDI32.INSTANCE.DeleteDC(hdc); + if (logical != 0 && logical / actual > 1) { + return (double) logical / actual; + } + } + return Toolkit.getDefaultToolkit().getScreenResolution() / 96.0d; + } + public static void main(String[] args) { + // Do this before anything else runs to override OS-level dpi settings, + // since we have in-client scaling now (not applicable to macOS, which + // implements OS-scaling in a different fashion) + if (!Util.isMacOS()) { + // Disable OS-level scaling in all JREs > 8 + System.setProperty("sun.java2d.uiScale.enabled", "false"); + System.setProperty("sun.java2d.uiScale", "1"); + + // Required for newer versions of Oracle 8 to disable OS-level scaling + System.setProperty("sun.java2d.dpiaware", "true"); + + // Account for OS scaling on modern versions of Windows + if (Util.isModernWindowsOS()) { + OSScalingFactor = getScaleFactor(); + System.setProperty("flatlaf.uiScale", String.valueOf(OSScalingFactor)); + System.setProperty("flatlaf.useWindowDecorations", String.valueOf(false)); + + // Forcibly disable the nimbus theme if selected, when the scaling factor is not 1.0 + if (OSScalingFactor != 1.0) { + forceDisableNimbus = true; + } + } + + // Linux / other + if (!Util.isWindowsOS()) { + System.setProperty("GDK_SCALE", "1"); + } + } + + if (Util.isMacOS()) { + // Note: This only works on some Java 8 implementations... Hopefully + // AdoptOpenJDK/Azul 8 fix their support for this eventually. + System.setProperty("apple.awt.application.appearance", "system"); + System.setProperty("apple.awt.application.name", "RSCPlus"); + } + + numCores = Runtime.getRuntime().availableProcessors(); + Logger.start(); Settings.initDir(); Properties props = Settings.initSettings(); @@ -354,6 +510,7 @@ public static void main(String[] args) { + "You may encounter additional bugs, for best results use version 8."); } + setScaledWindow(ScaledWindow.getInstance()); setConfigWindow(new ConfigWindow()); Settings.loadKeybinds(props); Settings.successfullyInitted = true; @@ -444,6 +601,16 @@ public static void setConfigWindow(ConfigWindow configWindow) { Launcher.configWindow = configWindow; } + /** @return the window */ + public static ScaledWindow getScaledWindow() { + return scaledWindow; + } + + /** @param scaledWindow the window to set */ + public static void setScaledWindow(ScaledWindow scaledWindow) { + Launcher.scaledWindow = scaledWindow; + } + /** @return the window */ public static WorldMapWindow getWorldMapWindow() { return worldMapWindow; diff --git a/src/Client/NotificationsHandler.java b/src/Client/NotificationsHandler.java index b9079b8..d4823e4 100644 --- a/src/Client/NotificationsHandler.java +++ b/src/Client/NotificationsHandler.java @@ -18,8 +18,9 @@ */ package Client; +import static Client.Util.osScaleMul; + import Game.Client; -import Game.Game; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; @@ -27,6 +28,7 @@ import java.awt.FontFormatException; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; +import java.awt.Image; import java.awt.Rectangle; import java.awt.SystemTray; import java.awt.TrayIcon.MessageType; @@ -70,12 +72,9 @@ public class NotificationsHandler { public enum NotifType { PM, TRADE, - DUEL, UNDER_ATTACK, - LOGOUT, LOWHP, - FATIGUE, - HIGHLIGHTEDITEM + HIGHLIGHTEDITEM // TODO: Implement } /** Initializes the Notification JFrame and prepares it to receive notifications */ @@ -130,19 +129,25 @@ private static void runInit() { // 3 JPanel iconPanel = new JPanel(); - iconPanel.setBounds(0, 0, 79, 79); + iconPanel.setBounds(0, 0, osScaleMul(79), osScaleMul(79)); iconPanel.setLayout(new BorderLayout(0, 0)); // 4 iconLabel = new JLabel(); - iconLabel.setIcon(new ImageIcon(Launcher.getResource("/assets/icon.png"))); + Image iconImg = new ImageIcon(Launcher.getResource("/assets/icon.png")).getImage(); + iconLabel.setIcon( + new ImageIcon( + iconImg.getScaledInstance( + osScaleMul(iconImg.getWidth(null)), + osScaleMul(iconImg.getHeight(null)), + Image.SCALE_DEFAULT))); iconLabel.setHorizontalAlignment(SwingConstants.CENTER); iconLabel.setVerticalAlignment(SwingConstants.CENTER); iconPanel.add(iconLabel, BorderLayout.CENTER); // 5 notificationTitle = new JLabel(); - notificationTitle.setBounds(91, 3, 326, 26); + notificationTitle.setBounds(osScaleMul(91), osScaleMul(3), osScaleMul(326), osScaleMul(26)); notificationTitle.setForeground(new Color(0x1d, 0x1d, 0x1d)); mainContentPanel.add(notificationTitle); @@ -154,7 +159,7 @@ private static void runInit() { notificationTextArea.setEditable(false); notificationTextArea.setBorder(null); notificationTextArea.setLineWrap(true); - notificationTextArea.setBounds(91, 30, 326, 43); + notificationTextArea.setBounds(osScaleMul(91), osScaleMul(30), osScaleMul(326), osScaleMul(43)); notificationTextArea.addMouseListener(mouseManager); // 7 @@ -167,10 +172,24 @@ public void actionPerformed(ActionEvent arg0) { setNotificationWindowVisible(false); } }); - closeButton.setBounds(400, 5, 17, 17); - closeButton.setIcon(new ImageIcon(Launcher.getResource("/assets/notification_close.png"))); + closeButton.setBounds(osScaleMul(400), osScaleMul(5), osScaleMul(17), osScaleMul(17)); + Image closeButtonImg = + new ImageIcon(Launcher.getResource("/assets/notification_close.png")).getImage(); + closeButton.setIcon( + new ImageIcon( + closeButtonImg.getScaledInstance( + osScaleMul(closeButtonImg.getWidth(null)), + osScaleMul(closeButtonImg.getHeight(null)), + Image.SCALE_DEFAULT))); + Image closeHighlightButtonImg = + new ImageIcon(Launcher.getResource("/assets/notification_close_highlighted.png")) + .getImage(); closeButton.setSelectedIcon( - new ImageIcon(Launcher.getResource("/assets/notification_close_highlighted.png"))); + new ImageIcon( + closeHighlightButtonImg.getScaledInstance( + osScaleMul(closeHighlightButtonImg.getWidth(null)), + osScaleMul(closeHighlightButtonImg.getHeight(null)), + Image.SCALE_DEFAULT))); closeButton.setBorder(BorderFactory.createEmptyBorder()); closeButton.setContentAreaFilled(false); mainContentPanel.add(closeButton); @@ -186,16 +205,24 @@ public void actionPerformed(ActionEvent arg0) { // Configure the frame to have rounded corners and to be transparent notificationFrame.setShape( new RoundRectangle2D.Double( - 0, 0, notificationFrame.getWidth(), notificationFrame.getHeight(), 16, 16)); + 0, + 0, + osScaleMul(notificationFrame.getWidth()), + osScaleMul(notificationFrame.getHeight()), + osScaleMul(16), + osScaleMul(16))); notificationFrame.setBackground(new Color(0, 0, 0, 0)); // Make the JFrame itself transparent. contentPanel.setBackground(new Color(0, 0, 0, 0)); - notificationFrame.setBounds(width - 446, height - 154, 449, 104); - notificationFrame.setMaximumSize(new Dimension(449, 104)); - notificationFrame.setMaximizedBounds(new Rectangle(width - 446, height - 154, 449, 104)); + notificationFrame.setBounds( + width - osScaleMul(446), height - osScaleMul(154), osScaleMul(449), osScaleMul(104)); + notificationFrame.setMaximumSize(osScaleMul(new Dimension(449, 104))); + notificationFrame.setMaximizedBounds( + new Rectangle( + width - osScaleMul(446), height - osScaleMul(154), osScaleMul(449), osScaleMul(104))); // 2 - mainContentPanel.setBounds(13, 13, 423, 79); + mainContentPanel.setBounds(osScaleMul(13), osScaleMul(13), osScaleMul(423), osScaleMul(79)); mainContentPanel.setBackground(new Color(249, 249, 247, 0)); contentPanel.add(mainContentPanel); // To make sure it's added at a reasonable time @@ -217,12 +244,16 @@ public void actionPerformed(ActionEvent arg0) { // 8 (Add the background image JLabel backgroundImage = new JLabel(""); - ImageIcon img = null; - - img = new ImageIcon(Launcher.getResource("/assets/notification_background.png")); - backgroundImage.setBounds(0, 0, 442, 104); - - backgroundImage.setIcon(img); + Image backgroundImg = + new ImageIcon(Launcher.getResource("/assets/notification_background.png")).getImage(); + backgroundImage.setBounds(0, 0, osScaleMul(442), osScaleMul(104)); + + backgroundImage.setIcon( + new ImageIcon( + backgroundImg.getScaledInstance( + osScaleMul(backgroundImg.getWidth(null)), + osScaleMul(backgroundImg.getHeight(null)), + Image.SCALE_DEFAULT))); backgroundImage.setBackground(new Color(0, 0, 0, 0)); backgroundImage.setForeground(new Color(0, 0, 0, 0)); backgroundImage.setOpaque(false); @@ -269,8 +300,8 @@ public void actionPerformed(ActionEvent arg0) { Font.createFont( Font.TRUETYPE_FONT, Launcher.getResourceAsStream("/assets/OpenSans-Bold.ttf")); - notificationTitle.setFont(boldFont.deriveFont(Font.BOLD, 18f)); - notificationTextArea.setFont(font.deriveFont(Font.PLAIN, 16f)); + notificationTitle.setFont(boldFont.deriveFont(Font.BOLD, osScaleMul(18))); + notificationTextArea.setFont(font.deriveFont(Font.PLAIN, osScaleMul(16))); } catch (FontFormatException | IOException e) { Logger.Error("Error while setting up notifications font:" + e.getMessage()); e.printStackTrace(); @@ -339,11 +370,11 @@ public static synchronized long getLastNotifTime() { /** * Displays/plays a notification popup or sound. This method checks whether each of the respective * settings for that specific notification type.
- * This method does not check for values such as low HP or fatigue amounts, as the code - * that does so is local to the Render method. + * This method does not check for values such as low HP amounts, as the code that does so + * is local to the Render method. * - * @param type The NotifType to display. This can be one of SYSTEM, PM, TRADE, UNDER_ATTACK, - * LOGOUT, LOWHP, or FATIGUE as of the writing of this documentation. + * @param type The NotifType to display. This can be one of, PM, TRADE, UNDER_ATTACK, LOWHP, or + * HIGHLIGHTEDITEM as of the writing of this documentation. * @param title The title to use for the notification. * @param text Text message of the notification. * @return True if at least one type of notification (audio/popup) was attempted; false otherwise @@ -358,7 +389,7 @@ public static boolean notify(NotifType type, String title, String text) { if (Settings.NOTIFICATION_SOUNDS.get(Settings.currentProfile)) { // If always notification sounds or if game isn't focused, play audio if (Settings.SOUND_NOTIFS_ALWAYS.get(Settings.currentProfile) - || (!Game.getInstance().getContentPane().hasFocus())) { + || (!ScaledWindow.getInstance().hasFocus())) { playNotificationSound(); didNotify = true; } @@ -366,7 +397,7 @@ public static boolean notify(NotifType type, String title, String text) { if (Settings.TRAY_NOTIFS.get(Settings.currentProfile)) { // If always tray notifications or if game isn't focused, display tray notification if (Settings.TRAY_NOTIFS_ALWAYS.get(Settings.currentProfile) - || (!Game.getInstance().getContentPane().hasFocus())) { + || (!ScaledWindow.getInstance().hasFocus())) { displayNotification(title, text, "normal"); didNotify = true; } @@ -380,7 +411,7 @@ public static boolean notify(NotifType type, String title, String text) { if (Settings.NOTIFICATION_SOUNDS.get(Settings.currentProfile)) { // If always notification sounds or if game isn't focused, play audio if (Settings.SOUND_NOTIFS_ALWAYS.get(Settings.currentProfile) - || (!Game.getInstance().getContentPane().hasFocus())) { + || (!ScaledWindow.getInstance().hasFocus())) { playNotificationSound(); didNotify = true; } @@ -388,7 +419,7 @@ public static boolean notify(NotifType type, String title, String text) { if (Settings.TRAY_NOTIFS.get(Settings.currentProfile)) { // If always tray notifications or if game isn't focused, display tray notification if (Settings.TRAY_NOTIFS_ALWAYS.get(Settings.currentProfile) - || (!Game.getInstance().getContentPane().hasFocus())) { + || (!ScaledWindow.getInstance().hasFocus())) { displayNotification(title, text, "normal"); didNotify = true; } @@ -402,7 +433,7 @@ public static boolean notify(NotifType type, String title, String text) { if (Settings.NOTIFICATION_SOUNDS.get(Settings.currentProfile)) { // If always notification sounds or if game isn't focused, play audio if (Settings.SOUND_NOTIFS_ALWAYS.get(Settings.currentProfile) - || (!Game.getInstance().getContentPane().hasFocus())) { + || (!ScaledWindow.getInstance().hasFocus())) { playNotificationSound(); didNotify = true; } @@ -410,7 +441,7 @@ public static boolean notify(NotifType type, String title, String text) { if (Settings.TRAY_NOTIFS.get(Settings.currentProfile)) { // If always tray notifications or if game isn't focused, display tray notification if (Settings.TRAY_NOTIFS_ALWAYS.get(Settings.currentProfile) - || (!Game.getInstance().getContentPane().hasFocus())) { + || (!ScaledWindow.getInstance().hasFocus())) { displayNotification(title, text, "normal"); didNotify = true; } @@ -418,57 +449,13 @@ public static boolean notify(NotifType type, String title, String text) { } break; } - case LOGOUT: - { - if (Settings.LOGOUT_NOTIFICATIONS.get(Settings.currentProfile)) { - if (Settings.NOTIFICATION_SOUNDS.get(Settings.currentProfile)) { - // If always notification sounds or if game isn't focused, play audio - if (Settings.SOUND_NOTIFS_ALWAYS.get(Settings.currentProfile) - || (!Game.getInstance().getContentPane().hasFocus())) { - playNotificationSound(); - didNotify = true; - } - } - if (Settings.TRAY_NOTIFS.get(Settings.currentProfile)) { - // If always tray notifications or if game isn't focused, display tray notification - if (Settings.TRAY_NOTIFS_ALWAYS.get(Settings.currentProfile) - || (!Game.getInstance().getContentPane().hasFocus())) { - displayNotification(title, text, "critical"); - didNotify = true; - } - } - } - break; - } case LOWHP: { if (Settings.LOW_HP_NOTIFICATIONS.get(Settings.currentProfile)) { if (Settings.NOTIFICATION_SOUNDS.get(Settings.currentProfile)) { // If always notification sounds or if game isn't focused, play audio if (Settings.SOUND_NOTIFS_ALWAYS.get(Settings.currentProfile) - || (!Game.getInstance().getContentPane().hasFocus())) { - playNotificationSound(); - didNotify = true; - } - } - if (Settings.TRAY_NOTIFS.get(Settings.currentProfile)) { - // If always tray notifications or if game isn't focused, display tray notification - if (Settings.TRAY_NOTIFS_ALWAYS.get(Settings.currentProfile) - || (!Game.getInstance().getContentPane().hasFocus())) { - displayNotification(title, text, "critical"); - didNotify = true; - } - } - } - break; - } - case FATIGUE: - { - if (Settings.FATIGUE_NOTIFICATIONS.get(Settings.currentProfile)) { - if (Settings.NOTIFICATION_SOUNDS.get(Settings.currentProfile)) { - // If always notification sounds or if game isn't focused, play audio - if (Settings.SOUND_NOTIFS_ALWAYS.get(Settings.currentProfile) - || (!Game.getInstance().getContentPane().hasFocus())) { + || (!ScaledWindow.getInstance().hasFocus())) { playNotificationSound(); didNotify = true; } @@ -476,7 +463,7 @@ public static boolean notify(NotifType type, String title, String text) { if (Settings.TRAY_NOTIFS.get(Settings.currentProfile)) { // If always tray notifications or if game isn't focused, display tray notification if (Settings.TRAY_NOTIFS_ALWAYS.get(Settings.currentProfile) - || (!Game.getInstance().getContentPane().hasFocus())) { + || (!ScaledWindow.getInstance().hasFocus())) { displayNotification(title, text, "critical"); didNotify = true; } @@ -640,7 +627,7 @@ class NotifsShowGameMouseListener implements MouseListener { @Override public void mouseClicked(MouseEvent arg0) { - Game.getInstance().toFront(); + ScaledWindow.getInstance().toFront(); NotificationsHandler.setNotificationWindowVisible(false); } diff --git a/src/Client/ScaledWindow.java b/src/Client/ScaledWindow.java new file mode 100644 index 0000000..7a86bd3 --- /dev/null +++ b/src/Client/ScaledWindow.java @@ -0,0 +1,928 @@ +package Client; + +import Game.Client; +import Game.Game; +import Game.KeyboardHandler; +import Game.MouseHandler.BufferedMouseClick; +import Game.Renderer; +import java.applet.Applet; +import java.awt.*; +import java.awt.event.*; +import java.awt.geom.AffineTransform; +import java.awt.image.AffineTransformOp; +import java.awt.image.BufferedImage; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.swing.*; + +/** + * This class is responsible for rendering all output from the applet onto the screen, which it + * receives via a {@link BufferedImage} from the {@link Renderer#present} method. All window + * interactions are then forwarded to the applet contained within the {@link Game} class. + */ +public class ScaledWindow extends JFrame + implements WindowListener, + FocusListener, + ComponentListener, + MouseListener, + MouseMotionListener, + MouseWheelListener, + KeyListener { + + // Singleton + private static ScaledWindow instance = null; + private static boolean initialRender = true; + private static boolean isMacOS = false; + private static boolean shouldRealign = false; + private int frameWidth = 0; + private int frameHeight = 0; + private ScaledViewport scaledViewport; + private int viewportWidth = 0; + private int viewportHeight = 0; + private BufferedImage unscaledBackground = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); + private int previousUnscaledWidth; + private int previousUnscaledHeight; + + // Mouse click buffer used to prevent click de-syncs with custom overlays at low FPS + private static final Queue inputBuffer = new LinkedList<>(); + + /** Private constructor to ensure singleton nature */ + private ScaledWindow() { + try { + // Set System L&F as the default + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (UnsupportedLookAndFeelException e) { + Logger.Error("Unable to set L&F: Unsupported look and feel"); + } catch (ClassNotFoundException e) { + Logger.Error("Unable to set L&F: Class not found"); + } catch (InstantiationException e) { + Logger.Error("Unable to set L&F: Class object cannot be instantiated"); + } catch (IllegalAccessException e) { + Logger.Error("Unable to set L&F: Illegal access exception"); + } + + Logger.Info("Creating scaled window"); + + /* Initialize the contents of the frame. */ + try { + SwingUtilities.invokeAndWait( + new Runnable() { + @Override + public void run() { + runInit(); + } + }); + } catch (InvocationTargetException e) { + Logger.Error("There was a thread-related error while setting up the scaled window!"); + e.printStackTrace(); + } catch (InterruptedException e) { + Logger.Error( + "There was a thread-related error while setting up the scaled window! The window may not be initialized properly!"); + e.printStackTrace(); + } + } + + private void runInit() { + // Set window properties + setBackground(Color.black); + setFocusTraversalKeysEnabled(false); + + // Add window listeners + addWindowListener(this); + addComponentListener(this); + addFocusListener(this); + addKeyListener(this); + + // TODO: Uncomment when implemented + // Allows drag-n-dropping a replay onto the game window + // setDropTarget(ReplayQueue.dropReplays); + + // Enable macOS fullscreen button, if possible + isMacOS = Util.isMacOS(); + + if (isMacOS) { + try { + Class util = Class.forName("com.apple.eawt.FullScreenUtilities"); + Class params[] = new Class[] {Window.class, Boolean.TYPE}; + Method method = util.getMethod("setWindowCanFullScreen", params); + method.invoke(util, this, true); + } catch (Exception ignored) { + } + } + + // Set minimum size to applet size + setMinimumSize(new Dimension(512, 346 + Renderer.GAME_RENDER_OFFSET)); + + URL iconURL = Launcher.getResource("/assets/icon.png"); + if (iconURL != null) { + ImageIcon icon = new ImageIcon(iconURL); + setIconImage(icon.getImage()); + } + + // Initialize scaled view + scaledViewport = new ScaledViewport(); + + scaledViewport.addMouseListener(this); + scaledViewport.addMouseMotionListener(this); + scaledViewport.addMouseWheelListener(this); + + scaledViewport.setSize(getSize()); + scaledViewport.setBackground(Color.black); + scaledViewport.revalidate(); + scaledViewport.repaint(); + scaledViewport.setVisible(true); + + add(scaledViewport); + + pack(); + revalidate(); + repaint(); + } + + /** + * Keep track of frame dimensions internally to avoid possible thread-safety issues when needing + * to invoke a method that uses the frame size, immediately after setting it. + * + *

NOTE: Must always call setMinimumSize before invoking this method + */ + @Override + public void setSize(int width, int height) { + super.setSize(width, height); + frameWidth = width; + frameHeight = height; + } + + /** Sets a flag to align the window after resizing the applet */ + public void setWindowRealignmentIntent(boolean flag) { + shouldRealign = flag; + } + + /** + * Centers the window or pins it to the top of the screen, if the custom size exactly matches the + * available space. + */ + private void alignWindow() { + Rectangle currentScreenBounds = + getGraphicsConfiguration().getDevice().getDefaultConfiguration().getBounds(); + + int x = ((currentScreenBounds.width - frameWidth) / 2) + currentScreenBounds.x; + int y = ((currentScreenBounds.height - frameHeight) / 2) + currentScreenBounds.y; + + if (Settings.CUSTOM_CLIENT_SIZE.get(Settings.currentProfile)) { + int currentWidthSetting = Settings.CUSTOM_CLIENT_SIZE_X.get(Settings.currentProfile); + int currentHeightSetting = Settings.CUSTOM_CLIENT_SIZE_Y.get(Settings.currentProfile); + + Dimension maxWindowDimensions = getMaximumEffectiveWindowSize(); + + int maxWindowWidth = maxWindowDimensions.width; + int maxWindowHeight = maxWindowDimensions.height; + + if (currentWidthSetting == maxWindowWidth && currentHeightSetting == maxWindowHeight) { + // pin window to top + y = currentScreenBounds.y; + } + } + + // Set the window location + setLocation(x, y); + } + + /** + * Used to determine the user's maximum effective window size, taking the window's insets into + * consideration. + */ + public Dimension getMaximumEffectiveWindowSize() { + Dimension maximumWindowSize = getMaximumWindowSize(); + + // Subtract + int windowWidth = maximumWindowSize.width - getWindowWidthInsets(); + int windowHeight = maximumWindowSize.height - getWindowHeightInsets(); + + if (Util.isModernWindowsOS()) { + windowWidth += 16; + windowHeight += 8; + } + + return new Dimension(windowWidth, windowHeight); + } + + /** Used to determine the user's maximum window size */ + public Dimension getMaximumWindowSize() { + GraphicsConfiguration graphicsConfiguration = + getGraphicsConfiguration().getDevice().getDefaultConfiguration(); + Rectangle screenBounds = graphicsConfiguration.getBounds(); + Insets screenInsets = getToolkit().getScreenInsets(graphicsConfiguration); + + // Subtract the operating system insets from the current display's max bounds + int maxWidth = screenBounds.width - screenInsets.left - screenInsets.right; + int maxHeight = screenBounds.height - screenInsets.top - screenInsets.bottom; + + return new Dimension(maxWidth, maxHeight); + } + + /** Opens the window */ + public void launchScaledWindow() { + setLocationRelativeTo(null); + setVisible(true); + } + + /** + * Used to pass the viewport's {@link Graphics} object to the applet such that it will render to + * it instead of itself. + */ + public static Graphics hookLoadingGraphics() { + return getInstance().getGraphics(); + } + + /** + * Sets the {@link BufferedImage} that the window should display, which ultimately comes from + * {@link Renderer#present}. The first frame render will load the user's custom window size + * settings, if present. + */ + public void setGameImage(BufferedImage gameImage) { + if (gameImage == null) { + return; + } + + if (scaledViewport.isViewportImageLoaded()) { + if (initialRender) { + // Resize to the custom window size, if it is defined + if (Settings.CUSTOM_CLIENT_SIZE.get(Settings.currentProfile)) { + int customClientWidth = Settings.CUSTOM_CLIENT_SIZE_X.get(Settings.currentProfile); + int customClientHeight = Settings.CUSTOM_CLIENT_SIZE_Y.get(Settings.currentProfile); + + int frameWidth = customClientWidth + getWindowWidthInsets(); + int frameHeight = customClientHeight + getWindowHeightInsets(); + + setWindowRealignmentIntent(true); + + setMinimumSize(getMinimumWindowSizeForScalar()); + setSize(frameWidth, frameHeight); + } else { + // Otherwise, set the window size for the scalar (will be realigned in the method) + resizeWindowToScalar(); + } + + // Update the window size spinner minimum values + Launcher.getConfigWindow() + .updateCustomClientSizeMinValues(getMinimumViewportSizeForScalar()); + + initialRender = false; + } + + viewportWidth = gameImage.getWidth(); + viewportHeight = gameImage.getHeight(); + } + + if (Renderer.renderingScalar == 1.0f) { + // Unscaled client behavior + int newUnscaledWidth = gameImage.getWidth(); + int newUnscaledHeight = gameImage.getHeight(); + + if (previousUnscaledWidth != newUnscaledWidth + || previousUnscaledHeight != newUnscaledHeight) { + unscaledBackground = + new BufferedImage(newUnscaledWidth, newUnscaledHeight, gameImage.getType()); + + previousUnscaledWidth = newUnscaledWidth; + previousUnscaledHeight = newUnscaledHeight; + } + + // Draw onto a new BufferedImage to prevent flickering + Graphics2D g2d = (Graphics2D) unscaledBackground.getGraphics(); + g2d.drawImage(gameImage, 0, 0, null); + g2d.dispose(); + + scaledViewport.setViewportImage(unscaledBackground); + + scaledViewport.repaint(); + } else { + // Scaled client behavior + scaledViewport.setViewportImage(gameImage); + + int scaledWidth = Math.round(viewportWidth * Renderer.renderingScalar); + int scaledHeight = Math.round(viewportHeight * Renderer.renderingScalar); + + try { + SwingUtilities.invokeAndWait( + () -> scaledViewport.paintImmediately(0, 0, scaledWidth, scaledHeight)); + } catch (InterruptedException | InvocationTargetException ignored) { + // no-op + } + } + } + + public boolean isViewportLoaded() { + return scaledViewport.isViewportImageLoaded(); + } + + public int getWindowWidthInsets() { + return getInsets().left + getInsets().right; + } + + public int getWindowHeightInsets() { + return getInsets().top + getInsets().bottom; + } + + /** Resizes the window size for the scalar */ + public void resizeWindowToScalar() { + Dimension minimumWindowSizeForScalar = getMinimumWindowSizeForScalar(); + + if (!getSize().equals(minimumWindowSizeForScalar)) { + // Update the window size as necessary, which will in turn + // invoke the componentResized listener on this JFrame + setWindowRealignmentIntent(true); + + setMinimumSize(minimumWindowSizeForScalar); + setSize(minimumWindowSizeForScalar); + } else { + // Resize the viewport if the actual window size didn't change, since + // the componentResized listener won't get triggered in that case. + // e.g. custom size set to 1024x692, then scale x2 turned on + setMinimumSize(minimumWindowSizeForScalar); + resizeApplet(); + } + + // Update the window size spinner minimum values + Launcher.getConfigWindow().updateCustomClientSizeMinValues(getMinimumViewportSizeForScalar()); + } + + /** Determines the smallest window size for the scalar, including insets */ + private Dimension getMinimumWindowSizeForScalar() { + Dimension minimumViewPortSizeForScalar = getMinimumViewportSizeForScalar(); + + int frameWidth = minimumViewPortSizeForScalar.width + getWindowWidthInsets(); + int frameHeight = minimumViewPortSizeForScalar.height + getWindowHeightInsets(); + + return new Dimension(frameWidth, frameHeight); + } + + /** Sets the current window size based on the user's settings */ + public void updateCustomWindowSizeFromSettings() { + Dimension scaledMinimumWindowSize = getMinimumViewportSizeForScalar(); + + Settings.CUSTOM_CLIENT_SIZE_X.put(Settings.currentProfile, scaledMinimumWindowSize.width); + Settings.CUSTOM_CLIENT_SIZE_Y.put(Settings.currentProfile, scaledMinimumWindowSize.height); + + Settings.save(); + + // Update the custom client size width and height spinners + Launcher.getConfigWindow().synchronizeGuiValues(); + } + + /** Determines the minimum window size for the applet based on the scalar */ + public Dimension getMinimumViewportSizeForScalar() { + return new Dimension( + Math.round(512 * Renderer.renderingScalar), + Math.round((346 + Renderer.GAME_RENDER_OFFSET) * Renderer.renderingScalar)); + } + + /** Resizes the applet contained within {@link Game} */ + private void resizeApplet() { + if (Renderer.renderingScalar == 0.0f || !isViewportLoaded()) { + return; + } + + int newWidth = Math.round(scaledViewport.getWidth() / Renderer.renderingScalar); + int newHeight = Math.round(scaledViewport.getHeight() / Renderer.renderingScalar); + + Applet mudclient = Game.getInstance().getApplet(); + + if (mudclient != null) { + mudclient.setSize(newWidth, newHeight); + Renderer.resize(newWidth, newHeight - Renderer.GAME_RENDER_OFFSET); + } + + if (shouldRealign) { + setWindowRealignmentIntent(false); + alignWindow(); + } + } + + /** Resizes the mudclient if its dimensions don't match the current frame size */ + public void validateAppletSize() { + Applet mudclient = Game.getInstance().getApplet(); + + if (mudclient == null) return; + + int newWidth = Math.round(scaledViewport.getWidth() / Renderer.renderingScalar); + int newHeight = Math.round(scaledViewport.getHeight() / Renderer.renderingScalar); + + if (mudclient.getWidth() != newWidth || mudclient.getHeight() != newHeight) { + mudclient.setSize(newWidth, newHeight); + Renderer.resize(newWidth, newHeight); + } + } + + public void disposeJFrame() { + dispose(); + } + + public Queue getInputBuffer() { + return inputBuffer; + } + + /* + * WindowListener methods - forward to Game.java + */ + + @Override + public void windowClosed(WindowEvent e) { + Game.getInstance() + .dispatchEvent(new WindowEvent(Game.getInstance(), WindowEvent.WINDOW_CLOSED)); + } + + @Override + public void windowClosing(WindowEvent e) { + Game.getInstance() + .dispatchEvent(new WindowEvent(Game.getInstance(), WindowEvent.WINDOW_CLOSING)); + } + + @Override + public void windowOpened(WindowEvent e) {} + + @Override + public void windowDeactivated(WindowEvent e) {} + + @Override + public void windowActivated(WindowEvent e) {} + + @Override + public void windowDeiconified(WindowEvent e) {} + + @Override + public void windowIconified(WindowEvent e) {} + + /* + * FocusListener methods - forward to Game.java + */ + + @Override + public void focusGained(FocusEvent e) {} + + @Override + public void focusLost(FocusEvent e) { + if (Client.handler_keyboard == null || Renderer.renderingScalar == 0.0f) return; + + KeyboardHandler.keyUp = false; + KeyboardHandler.keyDown = false; + KeyboardHandler.keyLeft = false; + KeyboardHandler.keyRight = false; + KeyboardHandler.keyShift = false; + } + + /* + * ComponentListener methods + */ + + @Override + public void componentResized(ComponentEvent e) { + resizeApplet(); + + frameWidth = e.getComponent().getWidth(); + frameHeight = e.getComponent().getHeight(); + } + + @Override + public void componentMoved(ComponentEvent e) {} + + @Override + public void componentShown(ComponentEvent e) {} + + @Override + public void componentHidden(ComponentEvent e) {} + + /* + * MouseListener, MouseMotionListener, and MouseWheelListener methods + * - forward to Client.handler_mouse + */ + + @Override + public void mouseClicked(MouseEvent e) { + if (Client.handler_mouse == null || Renderer.renderingScalar == 0.0f) return; + + Client.handler_mouse.mouseClicked(mapMouseEvent(e)); + } + + @Override + public void mousePressed(MouseEvent e) { + if (Client.handler_mouse == null || Renderer.renderingScalar == 0.0f) return; + + Client.handler_mouse.mousePressed(mapMouseEvent(e)); + } + + @Override + public void mouseReleased(MouseEvent e) { + if (Client.handler_mouse == null || Renderer.renderingScalar == 0.0f) return; + + Client.handler_mouse.mouseReleased(mapMouseEvent(e)); + } + + @Override + public void mouseEntered(MouseEvent e) { + if (Client.handler_mouse == null || Renderer.renderingScalar == 0.0f) return; + + Client.handler_mouse.mouseEntered(mapMouseEvent(e)); + } + + @Override + public void mouseExited(MouseEvent e) { + if (Client.handler_mouse == null || Renderer.renderingScalar == 0.0f) return; + + Client.handler_mouse.mouseExited(mapMouseEvent(e)); + } + + @Override + public void mouseDragged(MouseEvent e) { + if (Client.handler_mouse == null || Renderer.renderingScalar == 0.0f) return; + + Client.handler_mouse.mouseDragged(mapMouseEvent(e)); + } + + @Override + public void mouseMoved(MouseEvent e) { + if (Client.handler_mouse == null || Renderer.renderingScalar == 0.0f) return; + + Client.handler_mouse.mouseMoved(mapMouseEvent(e)); + } + + private static MouseEvent mapMouseEvent(MouseEvent e) { + Component mouseEventSource = (Component) e.getSource(); + int mouseEventId = e.getID(); + long mouseEventWhen = e.getWhen(); + int mouseEventModifiers = e.getModifiers(); + int mappedMouseEventX = Math.round(e.getX() / Renderer.renderingScalar); + int mappedMouseEventY = Math.round(e.getY() / Renderer.renderingScalar); + int mouseEventXOnScreen = e.getXOnScreen(); + int mouseEventYOnScreen = e.getYOnScreen(); + int mouseEventClickCount = e.getClickCount(); + boolean mouseEventPopupTrigger = e.isPopupTrigger(); + int mouseEventButton = e.getButton(); + + return new MouseEvent( + mouseEventSource, + mouseEventId, + mouseEventWhen, + mouseEventModifiers, + mappedMouseEventX, + mappedMouseEventY, + mouseEventXOnScreen, + mouseEventYOnScreen, + mouseEventClickCount, + mouseEventPopupTrigger, + mouseEventButton); + } + + @Override + public void mouseWheelMoved(MouseWheelEvent e) { + if (Client.handler_mouse == null || Renderer.renderingScalar == 0.0f) return; + + Client.handler_mouse.mouseWheelMoved(mapMouseWheelEvent(e)); + } + + private static MouseWheelEvent mapMouseWheelEvent(MouseWheelEvent e) { + Component mouseWheelEventSource = (Component) e.getSource(); + int mouseWheelEventId = e.getID(); + long mouseWheelEventWhen = e.getWhen(); + int mouseWheelEventModifiers = e.getModifiers(); + int mappedMouseWheelEventX = Math.round(e.getX() / Renderer.renderingScalar); + int mappedMouseWheelEventY = Math.round(e.getY() / Renderer.renderingScalar); + int mouseWheelEventXOnScreen = e.getXOnScreen(); + int mouseWheelEventYOnScreen = e.getYOnScreen(); + int mouseWheelEventClickCount = e.getClickCount(); + boolean mouseWheelEventPopupTrigger = e.isPopupTrigger(); + int mouseWheelEventScrollType = e.getScrollType(); + int mouseWheelEventScrollAmount = e.getScrollAmount(); + int mouseWheelEventWheelRotation = e.getWheelRotation(); + double mouseWheelEventPreciseWheelRotation = e.getPreciseWheelRotation(); + + return new MouseWheelEvent( + mouseWheelEventSource, + mouseWheelEventId, + mouseWheelEventWhen, + mouseWheelEventModifiers, + mappedMouseWheelEventX, + mappedMouseWheelEventY, + mouseWheelEventXOnScreen, + mouseWheelEventYOnScreen, + mouseWheelEventClickCount, + mouseWheelEventPopupTrigger, + mouseWheelEventScrollType, + mouseWheelEventScrollAmount, + mouseWheelEventWheelRotation, + mouseWheelEventPreciseWheelRotation); + } + + /* + * KeyListener methods - forward to Client.handler_keyboard + */ + + @Override + public void keyTyped(KeyEvent e) { + if (Client.handler_keyboard == null || Renderer.renderingScalar == 0.0f) return; + + Client.handler_keyboard.keyTyped(e); + } + + @Override + public void keyPressed(KeyEvent e) { + if (Client.handler_keyboard == null || Renderer.renderingScalar == 0.0f) return; + + Client.handler_keyboard.keyPressed(e); + } + + @Override + public void keyReleased(KeyEvent e) { + if (Client.handler_keyboard == null || Renderer.renderingScalar == 0.0f) return; + + Client.handler_keyboard.keyReleased(e); + } + + /** + * Gets the scaled window instance. It makes one if one doesn't exist. + * + * @return The scaled window instance + */ + public static ScaledWindow getInstance() { + if (instance == null) { + synchronized (ScaledWindow.class) { + instance = new ScaledWindow(); + } + } + return instance; + } + + /* + * Image rendering + */ + + /** JPanel used for rendering the game viewport, with scaling capabilities */ + private static class ScaledViewport extends JPanel { + BufferedImage interpolationBackground = new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR); + BufferedImage viewportImage; + + int previousWidth = 0; + int previousHeight = 0; + + int newWidth; + int newHeight; + + public ScaledViewport() { + super(); + setOpaque(true); + setBackground(Color.black); + } + + /** Provides the game image to the viewport */ + public void setViewportImage(BufferedImage gameImage) { + viewportImage = gameImage; + } + + /** Ensures the viewport image has been set */ + public boolean isViewportImageLoaded() { + return viewportImage != null; + } + + @Override + protected void paintComponent(Graphics g) { + if (viewportImage == null + || getInstance().viewportWidth == 0 + || getInstance().viewportHeight == 0) { + return; + } + + // Do not perform any scaling operations at a 1.0x scalar + if (Renderer.renderingScalar == 1.0f) { + g.drawImage(viewportImage, 0, 0, null); + return; + } + + newWidth = Math.round(viewportImage.getWidth() * Renderer.renderingScalar); + newHeight = Math.round(viewportImage.getHeight() * Renderer.renderingScalar); + + // Nearest-neighbor scaling performs roughly 3x better when resized via drawImage(), + // whereas interpolation scaling performs better using AffineTransformOp. + if (isIntegerScaling()) { + // Workaround for direct drawImage warping which seems to only + // affect macOS on JDK 19 + if (isMacOS && Settings.javaVersion >= 19) { + g.setClip(0, 0, newWidth, newHeight); + } + + g.drawImage(viewportImage, 0, 0, newWidth, newHeight, null); + } else { + if (interpolationBackground == null) { + return; + } + + // Reset image background when the window properties have changed + if (previousWidth != newWidth || previousHeight != newHeight) { + interpolationBackground = + new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_3BYTE_BGR); + + previousWidth = newWidth; + previousHeight = newHeight; + } + + Graphics2D g2d = (Graphics2D) interpolationBackground.getGraphics(); + + // Only perform multi-threading when the number of cores + // is enough to support true parallelization + if (Launcher.numCores > 4) { + g2d.drawImage( + multiThreadedInterpolationScaling(viewportImage, newWidth, newHeight), 0, 0, null); + } else { + g2d.drawImage(affineTransformScale(viewportImage, newWidth, newHeight), 0, 0, null); + } + + g2d.dispose(); + + // Draw the interpolation-scaled image + g.drawImage(interpolationBackground, 0, 0, null); + } + } + + /** Scales a {@link BufferedImage} using interpolation algorithms across four threads */ + private BufferedImage multiThreadedInterpolationScaling( + BufferedImage originalImage, int width, int height) { + BufferedImage[] splitImages = splitImage(originalImage); + + CompletableFuture future0 = + CompletableFuture.supplyAsync(() -> interpolationScale(splitImages[0], width, height, 0)); + CompletableFuture future1 = + CompletableFuture.supplyAsync(() -> interpolationScale(splitImages[1], width, height, 1)); + CompletableFuture future2 = + CompletableFuture.supplyAsync(() -> interpolationScale(splitImages[2], width, height, 2)); + CompletableFuture future3 = + CompletableFuture.supplyAsync(() -> interpolationScale(splitImages[3], width, height, 3)); + + List scaledImages = + Stream.of(future0, future1, future2, future3) + .map(CompletableFuture::join) + .collect(Collectors.toList()); + + return stitchImageParts(scaledImages); + } + + /** + * Splits a {@link BufferedImage} into four equal parts, adding extra padding to account for + * sampling around the seams + */ + private static BufferedImage[] splitImage(BufferedImage originalImage) { + int offsetBuffer = 10; + + BufferedImage[] imageParts = new BufferedImage[4]; + + int widthPadding = (originalImage.getWidth() & 1) == 1 ? 1 : 0; + int heightPadding = (originalImage.getHeight() & 1) == 1 ? 1 : 0; + + originalImage = padImageIfNeeded(originalImage, widthPadding, heightPadding); + + int halfWidth = originalImage.getWidth() / 2; + int halfHeight = originalImage.getHeight() / 2; + + imageParts[0] = + originalImage.getSubimage(0, 0, halfWidth + offsetBuffer, halfHeight + offsetBuffer); + imageParts[1] = + originalImage.getSubimage( + halfWidth - offsetBuffer, 0, halfWidth + offsetBuffer, halfHeight + offsetBuffer); + imageParts[2] = + originalImage.getSubimage( + 0, halfHeight - offsetBuffer, halfWidth + offsetBuffer, halfHeight + offsetBuffer); + imageParts[3] = + originalImage.getSubimage( + halfWidth - offsetBuffer, + halfHeight - offsetBuffer, + halfWidth + offsetBuffer, + halfHeight + offsetBuffer); + + return imageParts; + } + + /** Pads a {@link BufferedImage} with extra pixels in preparation for even splits */ + private static BufferedImage padImageIfNeeded( + BufferedImage originalImage, int widthPadding, int heightPadding) { + if (widthPadding == 0 && heightPadding == 0) { + return originalImage; + } + + BufferedImage paddedImage = + new BufferedImage( + originalImage.getWidth() + widthPadding, + originalImage.getHeight() + heightPadding, + originalImage.getType()); + + Graphics2D g2d = (Graphics2D) paddedImage.getGraphics(); + g2d.drawImage(originalImage, 0, 0, null); + g2d.dispose(); + + return paddedImage; + } + + /** + * Scales a {@link BufferedImage} for interpolation stitching, removing extra padding used for + * sampling edges + */ + private static BufferedImage interpolationScale( + BufferedImage originalImage, int width, int height, int n) { + int scaledOffsetBuffer = (int) (10 * Renderer.renderingScalar); + + BufferedImage scaledImage = + affineTransformScale( + originalImage, (width / 2) + scaledOffsetBuffer, (height / 2) + scaledOffsetBuffer); + + if (n == 0) { + return scaledImage.getSubimage( + 0, + 0, + scaledImage.getWidth() - scaledOffsetBuffer, + scaledImage.getHeight() - scaledOffsetBuffer); + } else if (n == 1) { + return scaledImage.getSubimage( + scaledOffsetBuffer, + 0, + scaledImage.getWidth() - scaledOffsetBuffer, + scaledImage.getHeight() - scaledOffsetBuffer); + } else if (n == 2) { + return scaledImage.getSubimage( + 0, + scaledOffsetBuffer, + scaledImage.getWidth() - scaledOffsetBuffer, + scaledImage.getHeight() - scaledOffsetBuffer); + } else { + return scaledImage.getSubimage( + scaledOffsetBuffer, + scaledOffsetBuffer, + scaledImage.getWidth() - scaledOffsetBuffer, + scaledImage.getHeight() - scaledOffsetBuffer); + } + } + + /** Scales a {@link BufferedImage} using the {@link AffineTransform} method */ + public static BufferedImage affineTransformScale( + BufferedImage originalImage, int width, int height) { + int imageWidth = originalImage.getWidth(); + int imageHeight = originalImage.getHeight(); + + double scaleX = (double) width / imageWidth; + double scaleY = (double) height / imageHeight; + + AffineTransform scaleTransform = AffineTransform.getScaleInstance(scaleX, scaleY); + int scalingAlgorithm = Settings.SCALING_ALGORITHM.get(Settings.currentProfile); + AffineTransformOp scalingOp = new AffineTransformOp(scaleTransform, scalingAlgorithm); + + return scalingOp.filter( + originalImage, new BufferedImage(width, height, originalImage.getType())); + } + + /** Stitches multiple {@link BufferedImage}s onto one canvas */ + private static BufferedImage stitchImageParts(List imageParts) { + int maxHeight = 0; + int maxWidth = 0; + + for (BufferedImage imagePart : imageParts) { + int imageWidth = imagePart.getWidth(null); + int imageHeight = imagePart.getHeight(null); + + maxHeight = Math.max(maxHeight, imageHeight); + maxWidth = Math.max(maxWidth, imageWidth); + } + + BufferedImage canvas = + new BufferedImage(maxWidth * 2, maxHeight * 2, BufferedImage.TYPE_3BYTE_BGR); + Graphics g = canvas.getGraphics(); + + g.setColor(Color.black); + g.fillRect(0, 0, canvas.getWidth(null), canvas.getHeight(null)); + + int currCol = 0; + int currRow = 0; + + for (BufferedImage imagePart : imageParts) { + g.drawImage(imagePart, currCol * maxWidth, currRow * maxHeight, null); + currCol++; + + if (currCol >= 2) { + currCol = 0; + currRow++; + } + } + + return canvas; + } + + /** Checks whether the window should be integer scaling */ + private static boolean isIntegerScaling() { + return Settings.SCALING_ALGORITHM.get(Settings.currentProfile) + == AffineTransformOp.TYPE_NEAREST_NEIGHBOR + || !Settings.SCALED_CLIENT_WINDOW.get(Settings.currentProfile); + } + } +} diff --git a/src/Client/Settings.java b/src/Client/Settings.java index ba337ee..12ab212 100644 --- a/src/Client/Settings.java +++ b/src/Client/Settings.java @@ -26,10 +26,13 @@ import Game.XPBar; import java.awt.Cursor; import java.awt.Point; +import java.awt.image.AffineTransformOp; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -39,6 +42,7 @@ public class Settings { // Internally used variables public static boolean fovUpdateRequired; + public static boolean renderingScalarUpdateRequired; public static boolean versionCheckRequired = true; public static int javaVersion = 0; public static final double VERSION_NUMBER = 20221031.170000; @@ -59,6 +63,11 @@ public class Settings { public static HashMap CUSTOM_CLIENT_SIZE_X = new HashMap(); public static HashMap CUSTOM_CLIENT_SIZE_Y = new HashMap(); + public static HashMap SCALED_CLIENT_WINDOW = new HashMap(); + public static HashMap SCALING_ALGORITHM = new HashMap(); + public static HashMap INTEGER_SCALING_FACTOR = new HashMap(); + public static HashMap BILINEAR_SCALING_FACTOR = new HashMap(); + public static HashMap BICUBIC_SCALING_FACTOR = new HashMap(); public static HashMap CHECK_UPDATES = new HashMap(); public static HashMap REMIND_HOW_TO_OPEN_SETTINGS = new HashMap(); @@ -76,6 +85,10 @@ public class Settings { public static HashMap COLORIZE_CONSOLE_TEXT = new HashMap(); public static HashMap FOV = new HashMap(); public static HashMap SOFTWARE_CURSOR = new HashMap(); + public static HashMap SHIFT_SCROLL_CAMERA_ROTATION = + new HashMap(); + public static HashMap TRACKPAD_ROTATION_SENSITIVITY = + new HashMap(); public static HashMap FPS_LIMIT_ENABLED = new HashMap(); public static HashMap FPS_LIMIT = new HashMap(); public static HashMap AUTO_SCREENSHOT = new HashMap(); @@ -89,6 +102,8 @@ public class Settings { public static HashMap LOG_FORCE_LEVEL = new HashMap(); public static HashMap PREFERS_XDG_OPEN = new HashMap(); public static HashMap INVENTORY_FULL_ALERT = new HashMap(); + public static HashMap USE_DARK_FLATLAF = new HashMap(); + public static HashMap USE_NIMBUS_THEME = new HashMap(); //// Notifications public static HashMap TRAY_NOTIFS = new HashMap(); @@ -96,12 +111,10 @@ public class Settings { public static HashMap NOTIFICATION_SOUNDS = new HashMap(); public static HashMap SOUND_NOTIFS_ALWAYS = new HashMap(); public static HashMap USE_SYSTEM_NOTIFICATIONS = new HashMap(); - public static HashMap FATIGUE_NOTIFICATIONS = new HashMap(); public static HashMap PM_NOTIFICATIONS = new HashMap(); public static HashMap TRADE_NOTIFICATIONS = new HashMap(); public static HashMap UNDER_ATTACK_NOTIFICATIONS = new HashMap(); - public static HashMap LOGOUT_NOTIFICATIONS = new HashMap(); public static HashMap LOW_HP_NOTIFICATIONS = new HashMap(); public static HashMap LOW_HP_NOTIF_VALUE = new HashMap(); public static HashMap HIGHLIGHTED_ITEM_NOTIFICATIONS = @@ -282,23 +295,72 @@ public static void definePresets(Properties props) { "custom", getPropBoolean(props, "custom_client_size", CUSTOM_CLIENT_SIZE.get("default"))); CUSTOM_CLIENT_SIZE_X.put("vanilla", 512); - CUSTOM_CLIENT_SIZE_X.put("vanilla_resizable", 512); - CUSTOM_CLIENT_SIZE_X.put("lite", 512); - CUSTOM_CLIENT_SIZE_X.put("default", 512); - CUSTOM_CLIENT_SIZE_X.put("heavy", 512); - CUSTOM_CLIENT_SIZE_X.put("all", 512); + CUSTOM_CLIENT_SIZE_X.put("vanilla_resizable", 1024); + CUSTOM_CLIENT_SIZE_X.put("lite", 1024); + CUSTOM_CLIENT_SIZE_X.put("default", 1024); + CUSTOM_CLIENT_SIZE_X.put("heavy", 1024); + CUSTOM_CLIENT_SIZE_X.put("all", 1024); CUSTOM_CLIENT_SIZE_X.put( "custom", getPropInt(props, "custom_client_size_x", CUSTOM_CLIENT_SIZE_X.get("default"))); CUSTOM_CLIENT_SIZE_Y.put("vanilla", 357); - CUSTOM_CLIENT_SIZE_Y.put("vanilla_resizable", 357); - CUSTOM_CLIENT_SIZE_Y.put("lite", 357); - CUSTOM_CLIENT_SIZE_Y.put("default", 357); - CUSTOM_CLIENT_SIZE_Y.put("heavy", 357); - CUSTOM_CLIENT_SIZE_Y.put("all", 357); + CUSTOM_CLIENT_SIZE_Y.put("vanilla_resizable", 714); + CUSTOM_CLIENT_SIZE_Y.put("lite", 714); + CUSTOM_CLIENT_SIZE_Y.put("default", 714); + CUSTOM_CLIENT_SIZE_Y.put("heavy", 714); + CUSTOM_CLIENT_SIZE_Y.put("all", 714); CUSTOM_CLIENT_SIZE_Y.put( "custom", getPropInt(props, "custom_client_size_y", CUSTOM_CLIENT_SIZE_Y.get("default"))); + SCALED_CLIENT_WINDOW.put("vanilla", false); + SCALED_CLIENT_WINDOW.put("vanilla_resizable", true); + SCALED_CLIENT_WINDOW.put("lite", true); + SCALED_CLIENT_WINDOW.put("default", true); + SCALED_CLIENT_WINDOW.put("heavy", true); + SCALED_CLIENT_WINDOW.put("all", true); + SCALED_CLIENT_WINDOW.put( + "custom", + getPropBoolean(props, "enable_window_scaling", SCALED_CLIENT_WINDOW.get("default"))); + + SCALING_ALGORITHM.put("vanilla", AffineTransformOp.TYPE_NEAREST_NEIGHBOR); + SCALING_ALGORITHM.put("vanilla_resizable", AffineTransformOp.TYPE_NEAREST_NEIGHBOR); + SCALING_ALGORITHM.put("lite", AffineTransformOp.TYPE_NEAREST_NEIGHBOR); + SCALING_ALGORITHM.put("default", AffineTransformOp.TYPE_NEAREST_NEIGHBOR); + SCALING_ALGORITHM.put("heavy", AffineTransformOp.TYPE_NEAREST_NEIGHBOR); + SCALING_ALGORITHM.put("all", AffineTransformOp.TYPE_NEAREST_NEIGHBOR); + SCALING_ALGORITHM.put( + "custom", getPropInt(props, "scaling_algorithm", SCALING_ALGORITHM.get("default"))); + + INTEGER_SCALING_FACTOR.put("vanilla", 2); + INTEGER_SCALING_FACTOR.put("vanilla_resizable", 2); + INTEGER_SCALING_FACTOR.put("lite", 2); + INTEGER_SCALING_FACTOR.put("default", 2); + INTEGER_SCALING_FACTOR.put("heavy", 2); + INTEGER_SCALING_FACTOR.put("all", 2); + INTEGER_SCALING_FACTOR.put( + "custom", + getPropInt(props, "integer_scaling_factor", INTEGER_SCALING_FACTOR.get("default"))); + + BILINEAR_SCALING_FACTOR.put("vanilla", 1.5f); + BILINEAR_SCALING_FACTOR.put("vanilla_resizable", 1.5f); + BILINEAR_SCALING_FACTOR.put("lite", 1.5f); + BILINEAR_SCALING_FACTOR.put("default", 1.5f); + BILINEAR_SCALING_FACTOR.put("heavy", 1.5f); + BILINEAR_SCALING_FACTOR.put("all", 1.5f); + BILINEAR_SCALING_FACTOR.put( + "custom", + getPropFloat(props, "bilinear_scaling_factor", BILINEAR_SCALING_FACTOR.get("default"))); + + BICUBIC_SCALING_FACTOR.put("vanilla", 1.5f); + BICUBIC_SCALING_FACTOR.put("vanilla_resizable", 1.5f); + BICUBIC_SCALING_FACTOR.put("lite", 1.5f); + BICUBIC_SCALING_FACTOR.put("default", 1.5f); + BICUBIC_SCALING_FACTOR.put("heavy", 1.5f); + BICUBIC_SCALING_FACTOR.put("all", 1.5f); + BICUBIC_SCALING_FACTOR.put( + "custom", + getPropFloat(props, "bicubic_scaling_factor", BICUBIC_SCALING_FACTOR.get("default"))); + CHECK_UPDATES.put("vanilla", true); CHECK_UPDATES.put("vanilla_resizable", true); CHECK_UPDATES.put("lite", true); @@ -452,6 +514,28 @@ public static void definePresets(Properties props) { SOFTWARE_CURSOR.put( "custom", getPropBoolean(props, "software_cursor", SOFTWARE_CURSOR.get("default"))); + SHIFT_SCROLL_CAMERA_ROTATION.put("vanilla", false); + SHIFT_SCROLL_CAMERA_ROTATION.put("vanilla_resizable", false); + SHIFT_SCROLL_CAMERA_ROTATION.put("lite", false); + SHIFT_SCROLL_CAMERA_ROTATION.put("default", true); + SHIFT_SCROLL_CAMERA_ROTATION.put("heavy", true); + SHIFT_SCROLL_CAMERA_ROTATION.put("all", true); + SHIFT_SCROLL_CAMERA_ROTATION.put( + "custom", + getPropBoolean( + props, "shift_scroll_camera_rotation", SHIFT_SCROLL_CAMERA_ROTATION.get("default"))); + + TRACKPAD_ROTATION_SENSITIVITY.put("vanilla", 8); + TRACKPAD_ROTATION_SENSITIVITY.put("vanilla_resizable", 8); + TRACKPAD_ROTATION_SENSITIVITY.put("lite", 8); + TRACKPAD_ROTATION_SENSITIVITY.put("default", 8); + TRACKPAD_ROTATION_SENSITIVITY.put("heavy", 8); + TRACKPAD_ROTATION_SENSITIVITY.put("all", 8); + TRACKPAD_ROTATION_SENSITIVITY.put( + "custom", + getPropInt( + props, "trackpad_rotation_sensitivity", TRACKPAD_ROTATION_SENSITIVITY.get("default"))); + VIEW_DISTANCE.put("vanilla", 2300); VIEW_DISTANCE.put("vanilla_resizable", 3000); VIEW_DISTANCE.put("lite", 10000); @@ -543,6 +627,26 @@ public static void definePresets(Properties props) { PREFERS_XDG_OPEN.put( "custom", getPropBoolean(props, "prefers_xdg_open", PREFERS_XDG_OPEN.get("default"))); + boolean defaultDarkMode = shouldDefaultDarkMode(); + + USE_DARK_FLATLAF.put("vanilla", defaultDarkMode); + USE_DARK_FLATLAF.put("vanilla_resizable", defaultDarkMode); + USE_DARK_FLATLAF.put("lite", defaultDarkMode); + USE_DARK_FLATLAF.put("default", defaultDarkMode); + USE_DARK_FLATLAF.put("heavy", defaultDarkMode); + USE_DARK_FLATLAF.put("all", defaultDarkMode); + USE_DARK_FLATLAF.put( + "custom", getPropBoolean(props, "use_dark_flatlaf", USE_DARK_FLATLAF.get("default"))); + + USE_NIMBUS_THEME.put("vanilla", false); + USE_NIMBUS_THEME.put("vanilla_resizable", false); + USE_NIMBUS_THEME.put("lite", false); + USE_NIMBUS_THEME.put("default", false); + USE_NIMBUS_THEME.put("heavy", false); + USE_NIMBUS_THEME.put("all", false); + USE_NIMBUS_THEME.put( + "custom", getPropBoolean(props, "use_nimbus_theme", USE_NIMBUS_THEME.get("default"))); + INVENTORY_FULL_ALERT.put("vanilla", false); INVENTORY_FULL_ALERT.put("vanilla_resizable", false); INVENTORY_FULL_ALERT.put("lite", false); @@ -628,16 +732,6 @@ public static void definePresets(Properties props) { getPropBoolean( props, "under_attack_notifications", UNDER_ATTACK_NOTIFICATIONS.get("default"))); - LOGOUT_NOTIFICATIONS.put("vanilla", false); - LOGOUT_NOTIFICATIONS.put("vanilla_resizable", false); - LOGOUT_NOTIFICATIONS.put("lite", false); - LOGOUT_NOTIFICATIONS.put("default", true); - LOGOUT_NOTIFICATIONS.put("heavy", true); - LOGOUT_NOTIFICATIONS.put("all", true); - LOGOUT_NOTIFICATIONS.put( - "custom", - getPropBoolean(props, "logout_notifications", LOGOUT_NOTIFICATIONS.get("default"))); - LOW_HP_NOTIFICATIONS.put("vanilla", false); LOW_HP_NOTIFICATIONS.put("vanilla_resizable", false); LOW_HP_NOTIFICATIONS.put("lite", false); @@ -657,16 +751,6 @@ public static void definePresets(Properties props) { LOW_HP_NOTIF_VALUE.put( "custom", getPropInt(props, "low_hp_notif_value", LOW_HP_NOTIF_VALUE.get("default"))); - FATIGUE_NOTIFICATIONS.put("vanilla", false); - FATIGUE_NOTIFICATIONS.put("vanilla_resizable", false); - FATIGUE_NOTIFICATIONS.put("lite", false); - FATIGUE_NOTIFICATIONS.put("default", true); - FATIGUE_NOTIFICATIONS.put("heavy", true); - FATIGUE_NOTIFICATIONS.put("all", true); - FATIGUE_NOTIFICATIONS.put( - "custom", - getPropBoolean(props, "fatigue_notifications", FATIGUE_NOTIFICATIONS.get("default"))); - HIGHLIGHTED_ITEM_NOTIFICATIONS.put("vanilla", false); HIGHLIGHTED_ITEM_NOTIFICATIONS.put("vanilla_resizable", false); HIGHLIGHTED_ITEM_NOTIFICATIONS.put("lite", false); @@ -1108,6 +1192,24 @@ public static void definePresets(Properties props) { save("custom"); } + if (INTEGER_SCALING_FACTOR.get("custom") < (int) Renderer.minScalar) { + INTEGER_SCALING_FACTOR.put("custom", (int) Renderer.minScalar); + } else if (INTEGER_SCALING_FACTOR.get("custom") > (int) Renderer.maxIntegerScalar) { + INTEGER_SCALING_FACTOR.put("custom", (int) Renderer.maxIntegerScalar); + } + + if (BILINEAR_SCALING_FACTOR.get("custom") < Renderer.minScalar) { + BILINEAR_SCALING_FACTOR.put("custom", Renderer.minScalar); + } else if (BILINEAR_SCALING_FACTOR.get("custom") > Renderer.maxInterpolationScalar) { + BILINEAR_SCALING_FACTOR.put("custom", Renderer.maxInterpolationScalar); + } + + if (BICUBIC_SCALING_FACTOR.get("custom") < Renderer.minScalar) { + BICUBIC_SCALING_FACTOR.put("custom", Renderer.minScalar); + } else if (BICUBIC_SCALING_FACTOR.get("custom") > Renderer.maxInterpolationScalar) { + BICUBIC_SCALING_FACTOR.put("custom", Renderer.maxInterpolationScalar); + } + if (WORLD.get("custom") < 0) { WORLD.put("custom", 0); save("custom"); @@ -1133,6 +1235,21 @@ public static void definePresets(Properties props) { } } + /** + * Determine whether we should default to dark mode for the app interface + * + * @return {@code boolean} indicating whether dark mode should be used + */ + public static boolean shouldDefaultDarkMode() { + // Detect via JNA/registry for Windows + if (Util.isWindowsOS()) { + return Util.isWindowsOSDarkTheme(); + } + + // Default to dark mode for other OS's + return true; + } + public static void initDir() { // TODO: Consider moving to a more relevant place // Find JAR directory // TODO: Consider utilizing Util.makeDirectory() @@ -1167,7 +1284,16 @@ public static boolean processKeybindCommand(String commandName) { // TODO: if (Client.state != Client.STATE_LOGIN) Client.logout(); return true; case "screenshot": - // TODO: Renderer.takeScreenshot(false); + Renderer.takeScreenshot(false); + return true; + case "toggle_scaling": + Settings.toggleWindowScaling(); + return true; + case "increase_scale": + Settings.increaseScale(); + return true; + case "decrease_scale": + Settings.decreaseScale(); return true; case "toggle_indicators": // TODO: Settings.toggleLagIndicator(); @@ -1178,6 +1304,9 @@ public static boolean processKeybindCommand(String commandName) { case "reset_rotation": // TODO: Camera.resetRotation(); return true; + case "toggle_trackpad_camera_rotation": + Settings.toggleTrackpadRotation(); + return true; case "toggle_colorize": // TODO: Settings.toggleColorTerminal(); return true; @@ -1248,10 +1377,10 @@ public static boolean processKeybindCommand(String commandName) { // TODO: Settings.toggleXpDrops(); return true; case "show_config_window": - Launcher.getConfigWindow().showConfigWindow(); + Launcher.getConfigWindow().toggleConfigWindow(); return true; case "show_worldmap_window": - Launcher.getWorldMapWindow().showWorldMapWindow(); + Launcher.getWorldMapWindow().toggleWorldMapWindow(); return true; case "show_queue_window": // Try to not allow Replay window to appear while logged into the game :-) @@ -1325,6 +1454,163 @@ public static boolean processKeybindCommand(String commandName) { return false; } + public static void toggleWindowScaling() { + if (ScaledWindow.getInstance().isViewportLoaded()) { + SCALED_CLIENT_WINDOW.put( + currentProfile, new Boolean(!SCALED_CLIENT_WINDOW.get(currentProfile))); + + if (SCALED_CLIENT_WINDOW.get(currentProfile)) { + Client.displayMessage("@cya@Client scaling is now enabled", Client.CHAT_NONE); + } else { + Client.displayMessage("@cya@Client scaling is now disabled", Client.CHAT_NONE); + } + + Settings.renderingScalarUpdateRequired = true; + + save(); + } + } + + public static void increaseScale() { + if (ScaledWindow.getInstance().isViewportLoaded()) { + float scalingDelta = 0f; + + if (!SCALED_CLIENT_WINDOW.get(currentProfile)) { + Client.displayMessage( + "@cya@Enable client scaling before attempting to increase the scale value", + Client.CHAT_NONE); + return; + } + + String scaleLimitReached = "@cya@Cannot increase the scale further"; + + if (SCALING_ALGORITHM.get(currentProfile).equals(AffineTransformOp.TYPE_NEAREST_NEIGHBOR)) { + int currentIntegerScalingFactor = INTEGER_SCALING_FACTOR.get(currentProfile); + scalingDelta = 1.0f; + + if (currentIntegerScalingFactor < (int) Renderer.maxIntegerScalar) { + int newScale = currentIntegerScalingFactor + (int) scalingDelta; + + INTEGER_SCALING_FACTOR.put(currentProfile, newScale); + Client.displayMessage("@cya@Increased scale to " + newScale + "x", Client.CHAT_NONE); + } else { + Client.displayMessage(scaleLimitReached, Client.CHAT_NONE); + return; + } + } else if (SCALING_ALGORITHM.get(currentProfile).equals(AffineTransformOp.TYPE_BILINEAR)) { + float currentBilinearScalingFactor = BILINEAR_SCALING_FACTOR.get(currentProfile); + scalingDelta = 0.1f; + + if (currentBilinearScalingFactor < Renderer.maxInterpolationScalar) { + float newScale = + BigDecimal.valueOf(currentBilinearScalingFactor + scalingDelta) + .setScale(1, RoundingMode.HALF_DOWN) + .floatValue(); + BILINEAR_SCALING_FACTOR.put(currentProfile, newScale); + Client.displayMessage("@cya@Increased scale to " + newScale + "x", Client.CHAT_NONE); + } else { + Client.displayMessage(scaleLimitReached, Client.CHAT_NONE); + return; + } + } else if (SCALING_ALGORITHM.get(currentProfile).equals(AffineTransformOp.TYPE_BICUBIC)) { + float currentBicubicScalingFactor = BICUBIC_SCALING_FACTOR.get(currentProfile); + scalingDelta = 0.1f; + + if (currentBicubicScalingFactor < Renderer.maxInterpolationScalar) { + float newScale = + BigDecimal.valueOf(currentBicubicScalingFactor + scalingDelta) + .setScale(1, RoundingMode.HALF_DOWN) + .floatValue(); + BICUBIC_SCALING_FACTOR.put(currentProfile, newScale); + Client.displayMessage("@cya@Increased scale to " + newScale + "x", Client.CHAT_NONE); + } else { + Client.displayMessage(scaleLimitReached, Client.CHAT_NONE); + return; + } + } + + Settings.renderingScalarUpdateRequired = true; + + save(); + } + } + + public static void decreaseScale() { + if (ScaledWindow.getInstance().isViewportLoaded()) { + float scalingDelta = 0; + + if (!SCALED_CLIENT_WINDOW.get(currentProfile)) { + Client.displayMessage( + "@cya@Enable client scaling before attempting to decrease the scale value", + Client.CHAT_NONE); + return; + } + + String scaleLimitReached = "@cya@Cannot decrease the scale further"; + + if (SCALING_ALGORITHM.get(currentProfile).equals(AffineTransformOp.TYPE_NEAREST_NEIGHBOR)) { + int currentIntegerScalingFactor = INTEGER_SCALING_FACTOR.get(currentProfile); + scalingDelta = 1.0f; + + if (currentIntegerScalingFactor > (int) Renderer.minScalar) { + int newScale = currentIntegerScalingFactor - (int) scalingDelta; + INTEGER_SCALING_FACTOR.put(currentProfile, newScale); + Client.displayMessage("@cya@Decreased scale to " + newScale + "x", Client.CHAT_NONE); + } else { + Client.displayMessage(scaleLimitReached, Client.CHAT_NONE); + return; + } + } else if (SCALING_ALGORITHM.get(currentProfile).equals(AffineTransformOp.TYPE_BILINEAR)) { + float currentBilinearScalingFactor = BILINEAR_SCALING_FACTOR.get(currentProfile); + scalingDelta = 0.1f; + + if (currentBilinearScalingFactor > Renderer.minScalar) { + float newScale = + BigDecimal.valueOf(currentBilinearScalingFactor - scalingDelta) + .setScale(1, RoundingMode.HALF_DOWN) + .floatValue(); + BILINEAR_SCALING_FACTOR.put(currentProfile, newScale); + Client.displayMessage("@cya@Decreased scale to " + newScale + "x", Client.CHAT_NONE); + } else { + Client.displayMessage(scaleLimitReached, Client.CHAT_NONE); + return; + } + } else if (SCALING_ALGORITHM.get(currentProfile).equals(AffineTransformOp.TYPE_BICUBIC)) { + float currentBicubicScalingFactor = BICUBIC_SCALING_FACTOR.get(currentProfile); + scalingDelta = 0.1f; + + if (currentBicubicScalingFactor > Renderer.minScalar) { + float newScale = + BigDecimal.valueOf(currentBicubicScalingFactor - scalingDelta) + .setScale(1, RoundingMode.HALF_DOWN) + .floatValue(); + BICUBIC_SCALING_FACTOR.put(currentProfile, newScale); + Client.displayMessage("@cya@Decreased scale to " + newScale + "x", Client.CHAT_NONE); + } else { + Client.displayMessage(scaleLimitReached, Client.CHAT_NONE); + return; + } + } + + Settings.renderingScalarUpdateRequired = true; + + save(); + } + } + + public static void toggleTrackpadRotation() { + SHIFT_SCROLL_CAMERA_ROTATION.put( + currentProfile, !SHIFT_SCROLL_CAMERA_ROTATION.get(currentProfile)); + + if (SHIFT_SCROLL_CAMERA_ROTATION.get(currentProfile)) { + Client.displayMessage("@cya@Trackpad Camera Rotation is now enabled", Client.CHAT_NONE); + } else { + Client.displayMessage("@cya@Trackpad Camera Rotation is now disabled", Client.CHAT_NONE); + } + + save(); + } + public static void toggleGoalBar() { SHOW_XP_BAR.put(currentProfile, !SHOW_XP_BAR.get(currentProfile)); if (SHOW_XP_BAR.get(currentProfile)) @@ -1341,20 +1627,20 @@ public static void toggleGoalBarPin() { save(); } - public static void checkSoftwareCursor(boolean initial) { - if (Renderer.lastDisplayingSoftwareCursor != Renderer.displayingSoftwareCursor || initial) { - if (SOFTWARE_CURSOR.get(currentProfile) && Renderer.displayingSoftwareCursor) { - Game.getInstance() - .setCursor( - Game.getInstance() - .getToolkit() - .createCustomCursor( - new BufferedImage(3, 3, BufferedImage.TYPE_INT_ARGB), - new Point(0, 0), - "null")); - } else { - Game.getInstance().setCursor(Cursor.getDefaultCursor()); - } + public static void checkSoftwareCursor() { + // Only load the software cursor if setting is enabled and + // the game has begun rendering post-loading + if (SOFTWARE_CURSOR.get(currentProfile) && ScaledWindow.getInstance().isViewportLoaded()) { + ScaledWindow.getInstance() + .setCursor( + ScaledWindow.getInstance() + .getToolkit() + .createCustomCursor( + new BufferedImage(3, 3, BufferedImage.TYPE_INT_ARGB), + new Point(0, 0), + "null")); + } else { + ScaledWindow.getInstance().setCursor(Cursor.getDefaultCursor()); } } @@ -1415,6 +1701,26 @@ private static int getPropInt(Properties props, String key, int defaultProp) { } } + /** + * Gets the Float value of a Properties object for the specified key. If no value is defined for + * that key, it returns the specified default value. + * + * @param props the Properties object to read + * @param key the name of the property to lookup + * @param defaultProp the default Float value of the specified property + * @return a Float value corresponding to the specified property + */ + private static float getPropFloat(Properties props, String key, float defaultProp) { + String value = props.getProperty(key); + if (value == null) return defaultProp; + + try { + return Float.parseFloat(value); + } catch (Exception e) { + return defaultProp; + } + } + /** * Gets the Boolean value of a Properties object for the specified key. If no value is defined for * that key, it returns the specified default value. @@ -1441,10 +1747,9 @@ private static boolean getPropBoolean(Properties props, String key, boolean defa * @return if it is recommended to use system notifications */ public static boolean isRecommendedToUseSystemNotifs() { - // Users on Windows 8.1 or 10 are recommend to set USE_SYSTEM_NOTIFICATIONS = true - if (System.getProperty("os.name").contains("Windows")) { - return "Windows 10".equals(System.getProperty("os.name")) - || "Windows 8.1".equals(System.getProperty("os.name")); + // Users on Windows 8.1+ are recommend to set USE_SYSTEM_NOTIFICATIONS = true + if (Util.isWindowsOS()) { + return Util.isModernWindowsOS(); } else { // Linux, macOS, etc. return NotificationsHandler.hasNotifySend; } @@ -1541,7 +1846,18 @@ public static void save(String preset) { props.setProperty("custom_client_size", Boolean.toString(CUSTOM_CLIENT_SIZE.get(preset))); props.setProperty("custom_client_size_x", Integer.toString(CUSTOM_CLIENT_SIZE_X.get(preset))); props.setProperty("custom_client_size_y", Integer.toString(CUSTOM_CLIENT_SIZE_Y.get(preset))); + props.setProperty( + "enable_window_scaling", Boolean.toString(SCALED_CLIENT_WINDOW.get(preset))); + props.setProperty("scaling_algorithm", Integer.toString(SCALING_ALGORITHM.get(preset))); + props.setProperty( + "integer_scaling_factor", Integer.toString(INTEGER_SCALING_FACTOR.get(preset))); + props.setProperty( + "bilinear_scaling_factor", Float.toString(BILINEAR_SCALING_FACTOR.get(preset))); + props.setProperty( + "bicubic_scaling_factor", Float.toString(BICUBIC_SCALING_FACTOR.get(preset))); props.setProperty("check_updates", Boolean.toString(CHECK_UPDATES.get(preset))); + props.setProperty("use_dark_flatlaf", Boolean.toString(USE_DARK_FLATLAF.get(preset))); + props.setProperty("use_nimbus_theme", Boolean.toString(USE_NIMBUS_THEME.get(preset))); props.setProperty( "welcome_enabled", Boolean.toString(REMIND_HOW_TO_OPEN_SETTINGS.get(preset))); props.setProperty("combat_menu", Boolean.toString(COMBAT_MENU_SHOWN.get(preset))); @@ -1569,6 +1885,12 @@ public static void save(String preset) { props.setProperty("fps_limit_enabled", Boolean.toString(FPS_LIMIT_ENABLED.get(preset))); props.setProperty("fps_limit", Integer.toString(FPS_LIMIT.get(preset))); props.setProperty("software_cursor", Boolean.toString(SOFTWARE_CURSOR.get(preset))); + props.setProperty( + "shift_scroll_camera_rotation", + Boolean.toString(SHIFT_SCROLL_CAMERA_ROTATION.get(preset))); + props.setProperty( + "trackpad_rotation_sensitivity", + Integer.toString(TRACKPAD_ROTATION_SENSITIVITY.get(preset))); props.setProperty("auto_screenshot", Boolean.toString(AUTO_SCREENSHOT.get(preset))); props.setProperty("view_distance", Integer.toString(VIEW_DISTANCE.get(preset))); props.setProperty("patch_gender", Boolean.toString(PATCH_GENDER.get(preset))); @@ -1632,7 +1954,6 @@ public static void save(String preset) { props.setProperty("trade_notifications", Boolean.toString(TRADE_NOTIFICATIONS.get(preset))); props.setProperty( "under_attack_notifications", Boolean.toString(UNDER_ATTACK_NOTIFICATIONS.get(preset))); - props.setProperty("logout_notifications", Boolean.toString(LOGOUT_NOTIFICATIONS.get(preset))); props.setProperty("low_hp_notifications", Boolean.toString(LOW_HP_NOTIFICATIONS.get(preset))); props.setProperty("low_hp_notif_value", Integer.toString(LOW_HP_NOTIF_VALUE.get(preset))); props.setProperty( @@ -1811,7 +2132,7 @@ public int compare(File o1, File o2) { int i = 1; if (fList != null) { for (File worldFile : fList) { - if (!worldFile.isDirectory()) { + if (!worldFile.isDirectory() && !worldFile.getName().equals(".DS_Store")) { Properties worldProps = new Properties(); try { FileInputStream in = new FileInputStream(worldFile); diff --git a/src/Client/TrayHandler.java b/src/Client/TrayHandler.java index 6f2b2b7..df3c2b7 100644 --- a/src/Client/TrayHandler.java +++ b/src/Client/TrayHandler.java @@ -18,8 +18,11 @@ */ package Client; +import static Client.Util.osScaleMul; + import Game.Game; import java.awt.AWTException; +import java.awt.Font; import java.awt.Image; import java.awt.MenuItem; import java.awt.PopupMenu; @@ -31,6 +34,7 @@ import java.awt.event.MouseListener; import java.awt.event.WindowEvent; import javax.imageio.ImageIO; +import javax.swing.JTabbedPane; /** Handles the creation of system tray icons and notifications */ public class TrayHandler implements MouseListener { @@ -59,22 +63,47 @@ public static void initTrayIcon() { return; } - tray = SystemTray.getSystemTray(); - trayIcon = - new TrayIcon( - trayIconImage.getScaledInstance(tray.getTrayIconSize().height, -1, Image.SCALE_SMOOTH)); + trayIcon = new TrayIcon(trayIconImage); trayIcon.addMouseListener(new TrayHandler()); + tray = SystemTray.getSystemTray(); + + Font scaledFont = new Font("sans-serif", Font.PLAIN, osScaleMul(12)); + // Create popup menu PopupMenu popup = new PopupMenu(); + + MenuItem about = new MenuItem("About RSCx"); + about.setFont(scaledFont); + + JTabbedPane settingsTabbedPane = Launcher.getConfigWindow().tabbedPane; + + int authorsTabIndex = settingsTabbedPane.indexOfTab("Authors"); + + if (authorsTabIndex > -1) { + about.addActionListener( + new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + settingsTabbedPane.setSelectedIndex(authorsTabIndex); + Launcher.getConfigWindow().showConfigWindow(); + } + }); + } else { + about.setEnabled(false); + } + MenuItem settings = new MenuItem("Settings"); + settings.setFont(scaledFont); MenuItem exit = new MenuItem("Exit"); + exit.setFont(scaledFont); settings.addActionListener( new ActionListener() { @Override public void actionPerformed(ActionEvent e) { + settingsTabbedPane.setSelectedIndex(0); Launcher.getConfigWindow().showConfigWindow(); } }); @@ -92,6 +121,8 @@ public void actionPerformed(ActionEvent e) { } }); + popup.add(about); + popup.addSeparator(); popup.add(settings); popup.add(exit); @@ -114,7 +145,7 @@ public static void removeTrayIcon() { @Override public void mouseClicked(MouseEvent e) { - Game.getInstance().toFront(); + ScaledWindow.getInstance().toFront(); } @Override diff --git a/src/Client/Util.java b/src/Client/Util.java index 9fc2a47..7e7edfe 100644 --- a/src/Client/Util.java +++ b/src/Client/Util.java @@ -18,6 +18,14 @@ */ package Client; +import com.formdev.flatlaf.FlatDarkLaf; +import com.formdev.flatlaf.FlatLaf; +import com.formdev.flatlaf.FlatLightLaf; +import com.sun.jna.platform.win32.Advapi32Util; +import com.sun.jna.platform.win32.WinReg; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; import java.awt.Point; import java.io.*; import java.nio.file.Files; @@ -25,6 +33,11 @@ import java.util.List; import java.util.Locale; import java.util.zip.CRC32; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; +import javax.swing.plaf.nimbus.NimbusLookAndFeel; /** A miscellaneous utility class */ public class Util { @@ -253,11 +266,136 @@ public static String findDirectoryReverse(String name) { return Settings.Dir.JAR; } + public static boolean isWindowsOS() { + return System.getProperty("os.name").contains("Windows"); + } + + public static boolean isModernWindowsOS() { + return "Windows 11".equals(System.getProperty("os.name")) + || "Windows 10".equals(System.getProperty("os.name")) + || "Windows 8.1".equals(System.getProperty("os.name")); + } + + /** + * Detects whether Windows OS is using dark mode + * + * @return {@code boolean} indicating dark mode usage or {@code false} if not on Windows + */ + public static boolean isWindowsOSDarkTheme() { + if (!isWindowsOS()) { + return false; + } + + final String REGISTRY_PATH = + "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; + final String REGISTRY_VALUE = "AppsUseLightTheme"; + + return Advapi32Util.registryValueExists(WinReg.HKEY_CURRENT_USER, REGISTRY_PATH, REGISTRY_VALUE) + && Advapi32Util.registryGetIntValue(WinReg.HKEY_CURRENT_USER, REGISTRY_PATH, REGISTRY_VALUE) + == 0; + } + + /** + * Sets the Swing look and feel for the window, using FlatLAF when appropriate. Certain global + * customizations to FlatLAF components are also set here. + */ + public static void setUITheme() { + try { + if (Util.shouldUseFLATLAFTheme()) { + // Register custom theme properties files + FlatLaf.registerCustomDefaultsSource(Launcher.getResource("/src/Client/FlatLaf/")); + + if (Settings.USE_DARK_FLATLAF.get(Settings.currentProfile)) { + UIManager.setLookAndFeel(new FlatDarkLaf()); + } else { + UIManager.setLookAndFeel(new FlatLightLaf()); + } + + // Customizations + UIManager.put("TabbedPane.showTabSeparators", true); + UIManager.put("TabbedPane.tabSeparatorsFullHeight", true); + } else { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + // Set System L&F as a fall-back option. + for (UIManager.LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) { + if ("Nimbus".equals(info.getName())) { + UIManager.setLookAndFeel(info.getClassName()); + NimbusLookAndFeel laf = (NimbusLookAndFeel) UIManager.getLookAndFeel(); + laf.getDefaults().put("defaultFont", new Font(Font.SANS_SERIF, Font.PLAIN, 11)); + laf.getDefaults().put("Table.alternateRowColor", new Color(230, 230, 255)); + break; + } + } + } + } catch (UnsupportedLookAndFeelException e) { + Logger.Error("Unable to set L&F: Unsupported look and feel"); + } catch (ClassNotFoundException e) { + Logger.Error("Unable to set L&F: Class not found"); + } catch (InstantiationException e) { + Logger.Error("Unable to set L&F: Class object cannot be instantiated"); + } catch (IllegalAccessException e) { + Logger.Error("Unable to set L&F: Illegal access exception"); + } + } + + /** @return {@code boolean} indicating whether FlatLAF should be used */ + public static boolean shouldUseFLATLAFTheme() { + // Unless forcibly disabled, check the corresponding setting + return Launcher.forceDisableNimbus || !Settings.USE_NIMBUS_THEME.get(Settings.currentProfile); + } + + /** @return {@code boolean} indicating if a FlatLAF theme is being used */ + public static boolean isUsingFlatLAFTheme() { + return isDarkThemeFlatLAF() || isLightThemeFlatLAF(); + } + + /** @return {@code boolean} indicating if the dark FlatLAF theme is being used */ + public static boolean isDarkThemeFlatLAF() { + return UIManager.getLookAndFeel().getClass().equals(FlatDarkLaf.class); + } + + /** @return {@code boolean} indicating if the light FlatLAF theme is being used */ + public static boolean isLightThemeFlatLAF() { + return UIManager.getLookAndFeel().getClass().equals(FlatLightLaf.class); + } + public static boolean isMacOS() { String os = System.getProperty("os.name").toLowerCase(Locale.ENGLISH); return (os.contains("mac") || os.contains("darwin")); } + /** + * Creates a scaled HTML message to be used within a {@link javax.swing.JOptionPane} + * + * @param htmlMessage {@link String} containing the HTML-formatted message to display + * @return The constructed {@link JPanel} instance + */ + public static JPanel createOptionMessagePanel(String htmlMessage) { + return createOptionMessagePanel(htmlMessage, null); + } + + /** + * Creates a scaled HTML message to be used within a {@link javax.swing.JOptionPane} + * + * @param htmlMessage {@link String} containing the HTML-formatted message to display + * @param stringArg Extra optional argument to the {@link String#format} method used within + * @return The constructed {@link JPanel} instance + */ + public static JPanel createOptionMessagePanel(String htmlMessage, String stringArg) { + JPanel panel = new JPanel(); + JLabel label = + new JLabel( + String.format( + "

" + + htmlMessage + + "

", + osScaleMul(10), + stringArg)); + panel.add(label); + + return panel; + } + public static String formatTimeDuration(int millis, int endMillis) { int seconds = (millis / 1000) % 60; int minutes = (millis / 1000 / 60) % 60; @@ -527,4 +665,72 @@ public static void openLinkInBrowser(String url) { Thread t = new Thread(new LinkOpener(url)); t.start(); } + + /* Utility methods for multiplying and dividing values by the Launcher.OSScalingFactor */ + + public static Dimension osScaleMul(Dimension d) { + if (Launcher.OSScalingFactor == 1.0) { + return d; + } + + d.setSize(osScaleMul(d.width), osScaleMul(d.height)); + return d; + } + + public static Dimension osScaleDiv(Dimension d) { + if (Launcher.OSScalingFactor == 1.0) { + return d; + } + + d.setSize(osScaleDiv(d.width), osScaleDiv(d.height)); + return d; + } + + public static int osScaleMul(int value) { + if (Launcher.OSScalingFactor == 1.0) { + return value; + } + + if (value == 0) { + return 0; + } + + return (int) (value * Launcher.OSScalingFactor); + } + + public static double osScaleMul(double value) { + if (Launcher.OSScalingFactor == 1.0) { + return value; + } + + if (value == 0) { + return 0; + } + + return (value * Launcher.OSScalingFactor); + } + + public static float osScaleMul(float value) { + if (Launcher.OSScalingFactor == 1.0) { + return value; + } + + if (value == 0) { + return 0; + } + + return (value * (float) Launcher.OSScalingFactor); + } + + public static int osScaleDiv(int value) { + if (Launcher.OSScalingFactor == 1.0) { + return value; + } + + if (value == 0) { + return 0; + } + + return (int) (value / Launcher.OSScalingFactor); + } } diff --git a/src/Client/WorldMapWindow.java b/src/Client/WorldMapWindow.java index b0d23b2..6eafcb7 100644 --- a/src/Client/WorldMapWindow.java +++ b/src/Client/WorldMapWindow.java @@ -1,10 +1,14 @@ package Client; +import static Client.Util.osScaleDiv; +import static Client.Util.osScaleMul; + import Game.Client; import Game.JGameData; import Game.Renderer; import java.awt.*; import java.awt.event.*; +import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.*; @@ -15,7 +19,6 @@ import java.util.Comparator; import javax.imageio.ImageIO; import javax.swing.*; -import javax.swing.plaf.nimbus.NimbusLookAndFeel; import org.json.JSONArray; import org.json.JSONObject; @@ -63,7 +66,7 @@ public CameraPoint(float x, float y) { private static SearchResult[] searchResults; private static boolean searchOverflow; private static boolean searchValid; - private static float zoom = 2.0f; + private static float zoom = osScaleMul(2.0f); private static boolean developmentMode; @@ -91,7 +94,8 @@ public CameraPoint(float x, float y) { private static Point playerPosition; private static int playerPlane; - private static int BORDER_SIZE = 8; + private static int BORDER_SIZE = osScaleMul(8); + private static int SCENERY_OFFSET_X = 48; private static int SCENERY_OFFSET_Y = 432; @@ -151,27 +155,7 @@ public CameraPoint(float x, float y) { }; public WorldMapWindow() { - try { - // Set System L&F as a fall-back option. - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - for (UIManager.LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) { - if ("Nimbus".equals(info.getName())) { - UIManager.setLookAndFeel(info.getClassName()); - NimbusLookAndFeel laf = (NimbusLookAndFeel) UIManager.getLookAndFeel(); - laf.getDefaults().put("defaultFont", new Font(Font.SANS_SERIF, Font.PLAIN, 11)); - laf.getDefaults().put("Table.alternateRowColor", new Color(230, 230, 255)); - break; - } - } - } catch (UnsupportedLookAndFeelException e) { - Logger.Error("Unable to set L&F: Unsupported look and feel"); - } catch (ClassNotFoundException e) { - Logger.Error("Unable to set L&F: Class not found"); - } catch (InstantiationException e) { - Logger.Error("Unable to set L&F: Class object cannot be instantiated"); - } catch (IllegalAccessException e) { - Logger.Error("Unable to set L&F: Illegal access exception"); - } + Util.setUITheme(); initialize(); } @@ -195,27 +179,30 @@ public void run() { e.printStackTrace(); } + int squareEdge = osScaleMul(24); + int buttonWidth = osScaleMul(116); + cameraPosition = new Point(0, 0); waypointPosition = null; - floorUpBounds = new Rectangle(0, 0, 24, 24); - floorDownBounds = new Rectangle(0, 0, 24, 24); - zoomUpBounds = new Rectangle(0, 0, 24, 24); - zoomDownBounds = new Rectangle(0, 0, 24, 24); + floorUpBounds = new Rectangle(0, 0, squareEdge, squareEdge); + floorDownBounds = new Rectangle(0, 0, squareEdge, squareEdge); + zoomUpBounds = new Rectangle(0, 0, squareEdge, squareEdge); + zoomDownBounds = new Rectangle(0, 0, squareEdge, squareEdge); floorTextBounds = new Point(0, 0); zoomTextBounds = new Point(0, 0); posTextBounds = new Point(0, 0); prevMousePoint = new Point(0, 0); prevMousePointMap = new Point(0, 0); playerPosition = new Point(0, 0); - chunkGridBounds = new Rectangle(0, 0, 116, 24); - showLabelsBounds = new Rectangle(0, 0, 116, 24); - showSceneryBounds = new Rectangle(0, 0, 116, 24); - showIconsBounds = new Rectangle(0, 0, 116, 24); - showOtherFloorsBounds = new Rectangle(0, 0, 116, 24); - followPlayerBounds = new Rectangle(0, 0, 116, 24); - legendBounds = new Rectangle(0, 0, 150, 24); - searchBounds = new Rectangle(0, 0, 250, 24); - searchRefreshBounds = new Rectangle(0, 0, 64, 24); + chunkGridBounds = new Rectangle(0, 0, buttonWidth, squareEdge); + showLabelsBounds = new Rectangle(0, 0, buttonWidth, squareEdge); + showSceneryBounds = new Rectangle(0, 0, buttonWidth, squareEdge); + showIconsBounds = new Rectangle(0, 0, buttonWidth, squareEdge); + showOtherFloorsBounds = new Rectangle(0, 0, buttonWidth, squareEdge); + followPlayerBounds = new Rectangle(0, 0, buttonWidth, squareEdge); + legendBounds = new Rectangle(0, 0, osScaleMul(150), squareEdge); + searchBounds = new Rectangle(0, 0, osScaleMul(250), squareEdge); + searchRefreshBounds = new Rectangle(0, 0, osScaleMul(64), squareEdge); planeIndex = 0; playerPlane = -1; followPlayer = true; @@ -380,9 +367,12 @@ private static void setAlpha(Graphics2D g, float alpha) { } private static void drawButton(Graphics2D g, String text, Rectangle bounds) { - g.setFont(Renderer.font_main); + g.setFont( + Renderer.font_main.deriveFont( + AffineTransform.getScaleInstance(Launcher.OSScalingFactor, Launcher.OSScalingFactor))); setAlpha(g, 0.5f); g.setColor(Renderer.color_text); + g.setStroke(new BasicStroke(osScaleMul(1))); g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height); setAlpha(g, 1.0f); g.setColor(Renderer.color_shadow); @@ -500,7 +490,9 @@ private static void drawSearch(Graphics2D g, String text, Rectangle bounds) { searchTerm += "*"; } - g.setFont(Renderer.font_main); + g.setFont( + Renderer.font_main.deriveFont( + AffineTransform.getScaleInstance(Launcher.OSScalingFactor, Launcher.OSScalingFactor))); g.setColor(Renderer.color_gray); setAlpha(g, 0.5f); g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height); @@ -511,17 +503,17 @@ private static void drawSearch(Graphics2D g, String text, Rectangle bounds) { g, searchTerm, bounds.x + BORDER_SIZE, - bounds.y + (bounds.height / 2) + 4, + bounds.y + (bounds.height / 2) + osScaleMul(4), searchColor, false); if (searchResults != null || searchOverflow) { // Calculate height int searchHeight; - if (searchOverflow || searchResults.length == 0) searchHeight = 16 + BORDER_SIZE; + if (searchOverflow || searchResults.length == 0) searchHeight = osScaleMul(16) + BORDER_SIZE; else if (searchResults.length <= SEARCH_RESULTS_MAX) - searchHeight = searchResults.length * 16 + BORDER_SIZE; - else searchHeight = (SEARCH_RESULTS_MAX + 1) * 16 + BORDER_SIZE; + searchHeight = searchResults.length * osScaleMul(16) + BORDER_SIZE; + else searchHeight = (SEARCH_RESULTS_MAX + 1) * osScaleMul(16) + BORDER_SIZE; if (searchHeight > 0) { g.setColor(Renderer.color_gray); @@ -556,7 +548,12 @@ else if (searchResults.length <= SEARCH_RESULTS_MAX) } Renderer.drawShadowText( - g, resultName, bounds.x + BORDER_SIZE, bounds.y - 8 - (i * 16), resultColor, false); + g, + resultName, + bounds.x + BORDER_SIZE, + bounds.y - osScaleMul(8) - (i * osScaleMul(16)), + resultColor, + false); // Only 1 result if (searchResults.length == 1) break; @@ -567,7 +564,7 @@ else if (searchResults.length <= SEARCH_RESULTS_MAX) g, "Too many results, narrow your search", bounds.x + BORDER_SIZE, - bounds.y - 8, + bounds.y - osScaleMul(8), Renderer.color_text, false); } else if (searchResults.length == 0) { @@ -575,7 +572,7 @@ else if (searchResults.length <= SEARCH_RESULTS_MAX) g, "No matches found", bounds.x + BORDER_SIZE, - bounds.y - 8, + bounds.y - osScaleMul(8), Renderer.color_text, false); } else { @@ -587,7 +584,7 @@ else if (searchResults.length <= SEARCH_RESULTS_MAX) g, Integer.toString(remaining) + " more " + resultText + "...", bounds.x + BORDER_SIZE, - bounds.y - 8 - (SEARCH_RESULTS_MAX * 16), + bounds.y - osScaleMul(8) - (SEARCH_RESULTS_MAX * osScaleMul(16)), Renderer.color_text, false); } @@ -620,10 +617,10 @@ private static void runInit() { // Initialize window frame = new JFrame(); frame.setTitle("World Map"); - frame.setBounds(0, 0, 800, 580); + frame.setBounds(0, 0, osScaleMul(800), osScaleMul(580)); frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); frame.getContentPane().setLayout(new BorderLayout()); - URL iconURL = Launcher.getResource("/assets/RSCX.logo.png"); + URL iconURL = Launcher.getResource("/assets/icon.png"); if (iconURL != null) { ImageIcon icon = new ImageIcon(iconURL); frame.setIconImage(icon.getImage()); @@ -1191,11 +1188,27 @@ public void showWorldMapWindow() { frame.setVisible(true); } + public void hideWorldMapWindow() { + frame.setVisible(false); + } + + public void toggleWorldMapWindow() { + if (isShown()) { + this.hideWorldMapWindow(); + } else { + this.showWorldMapWindow(); + } + } + + public static boolean isShown() { + return frame.isVisible(); + } + private static void setZoom(float val) { float prevZoom = zoom; zoom = val; if (zoom < 1.0f) zoom = 1.0f; - else if (zoom > 8.0f) zoom = 8.0f; + else if (zoom > osScaleMul(8.0f)) zoom = osScaleMul(8.0f); if ((!followPlayer || playerPlane == -1) && prevZoom < zoom) { float newX = (planes[0].getWidth(null)) - prevMousePointMap.x * 3; @@ -1395,6 +1408,7 @@ public static void drawMapLabel(Graphics2D g, int x, int y, MapLabel label) { && label.text.replaceAll("\n", " ").toLowerCase().contains(searchText.toLowerCase())) labelColor = Renderer.color_item_highlighted; + // Don't scale map labels Renderer.drawShadowText(g, line, x - offsetX + width / 2, y + offsetY, labelColor, false); y += height; } @@ -1416,16 +1430,16 @@ protected void paintComponent(Graphics graphics) { floorDownBounds.x = canvasWidth - floorDownBounds.width - BORDER_SIZE; floorDownBounds.y = canvasHeight - floorDownBounds.height - BORDER_SIZE; floorUpBounds.x = floorDownBounds.x; - floorUpBounds.y = floorDownBounds.y - 64; + floorUpBounds.y = floorDownBounds.y - osScaleMul(64); floorTextBounds.x = floorDownBounds.x + (floorDownBounds.width / 2); - floorTextBounds.y = floorDownBounds.y - 24; + floorTextBounds.y = floorDownBounds.y - osScaleMul(24); zoomDownBounds.x = floorUpBounds.x; zoomDownBounds.y = floorUpBounds.y - zoomDownBounds.height - BORDER_SIZE; zoomTextBounds.x = zoomDownBounds.x + (zoomDownBounds.width / 2); - zoomTextBounds.y = zoomDownBounds.y - 24; + zoomTextBounds.y = zoomDownBounds.y - osScaleMul(24); zoomUpBounds.x = zoomDownBounds.x; - zoomUpBounds.y = zoomDownBounds.y - 64; + zoomUpBounds.y = zoomDownBounds.y - osScaleMul(64); posTextBounds.x = BORDER_SIZE; posTextBounds.y = BORDER_SIZE * 2; @@ -1505,14 +1519,26 @@ protected void paintComponent(Graphics graphics) { if (waypointPosition != null) { Rectangle p = convertWorldCoordsToMap(waypointPosition.x, waypointPosition.y); setAlpha(g, p.width == planeIndex ? 1.0f : 0.25f); - g.setFont(fontsBold[18]); + g.setFont( + fontsBold[18].deriveFont( + AffineTransform.getScaleInstance( + Launcher.OSScalingFactor, Launcher.OSScalingFactor))); + AffineTransform currentTransform = g.getTransform(); + g.setTransform( + AffineTransform.getScaleInstance(Launcher.OSScalingFactor, Launcher.OSScalingFactor)); g.drawImage( waypointImage, - p.x - pointImage.getWidth(null) / 2 + (tileSize / 2), - p.y - pointImage.getHeight(null) / 2 + (tileSize / 2), + osScaleDiv(p.x - pointImage.getWidth(null) / 2 + (tileSize / 2)), + osScaleDiv(p.y - pointImage.getHeight(null) / 2 + (tileSize / 2)), null); + g.setTransform(currentTransform); Renderer.drawShadowText( - g, "Your destination", p.x + tileSize / 2, p.y - 28, Renderer.color_low, true); + g, + "Your destination", + p.x + tileSize / 2, + p.y - osScaleMul(28), + Renderer.color_low, + true); setAlpha(g, 1.0f); } @@ -1520,23 +1546,38 @@ protected void paintComponent(Graphics graphics) { if (playerPlane != -1) { setAlpha(g, playerPlane == planeIndex ? 1.0f : 0.25f); Rectangle p = convertWorldCoordsToMap(playerPosition.x, playerPosition.y); - g.setFont(fontsBold[18]); + g.setFont( + fontsBold[18].deriveFont( + AffineTransform.getScaleInstance( + Launcher.OSScalingFactor, Launcher.OSScalingFactor))); + AffineTransform currentTransform = g.getTransform(); + g.setTransform( + AffineTransform.getScaleInstance(Launcher.OSScalingFactor, Launcher.OSScalingFactor)); g.drawImage( pointImage, - p.x - pointImage.getWidth(null) / 2 + (tileSize / 2), - p.y - pointImage.getHeight(null) / 2 + (tileSize / 2), + osScaleDiv(p.x - pointImage.getWidth(null) / 2 + (tileSize / 2)), + osScaleDiv(p.y - pointImage.getHeight(null) / 2 + (tileSize / 2)), null); + g.setTransform(currentTransform); Renderer.drawShadowText( - g, "You are here", p.x + tileSize / 2, p.y - 28, Renderer.color_item_highlighted, true); + g, + "You are here", + p.x + tileSize / 2, + p.y - osScaleMul(28), + Renderer.color_item_highlighted, + true); setAlpha(g, 1.0f); } int renderY = posTextBounds.y; - g.setFont(Renderer.font_main); + g.setFont( + Renderer.font_main.deriveFont( + AffineTransform.getScaleInstance( + Launcher.OSScalingFactor, Launcher.OSScalingFactor))); if (developmentMode) { Renderer.drawShadowText( g, "DEVELOPMENT MODE", posTextBounds.x, renderY, Renderer.color_text, false); - renderY += 16; + renderY += osScaleMul(16); } Renderer.drawShadowText( g, @@ -1552,7 +1593,10 @@ protected void paintComponent(Graphics graphics) { drawButton(g, "+", zoomUpBounds); // Renderer.drawShadowText(g, "Floor", floorTextBounds.x, floorTextBounds.y - 54, // Renderer.color_text, true); - g.setFont(Renderer.font_big); + g.setFont( + Renderer.font_big.deriveFont( + AffineTransform.getScaleInstance( + Launcher.OSScalingFactor, Launcher.OSScalingFactor))); final String[] floorNames = {"G", "1", "2", "B"}; @@ -1613,15 +1657,15 @@ protected void paintComponent(Graphics graphics) { if (showLegend) { // First size legend and draw background - int startX = legendBounds.x - 150 + BORDER_SIZE; + int startX = legendBounds.x - osScaleMul(150) + BORDER_SIZE; int x = startX; int y = legendBounds.y + BORDER_SIZE; int offsetY = 0; - int maxY = 16 * 15; + int maxY = osScaleMul(16) * 15; for (int i = 0; i < legends.length; i++) { - offsetY += 16; + offsetY += osScaleMul(16); if (offsetY >= maxY) { - x += 150; + x += osScaleMul(150); offsetY = 0; } } @@ -1635,23 +1679,33 @@ protected void paintComponent(Graphics graphics) { x = legendBounds.x + BORDER_SIZE; y = legendBounds.y + BORDER_SIZE; offsetY = 0; + AffineTransform currentTransform = g.getTransform(); for (int i = 0; i < legends.length; i++) { + g.setTransform( + AffineTransform.getScaleInstance(Launcher.OSScalingFactor, Launcher.OSScalingFactor)); g.drawImage( legends[i], - x + (7 - legends[i].getWidth(null) / 2), - y + offsetY + (7 - legends[i].getHeight(null) / 2), + osScaleDiv(x + (osScaleMul(7) - osScaleMul(legends[i].getWidth(null) / 2))), + osScaleDiv( + y + offsetY + (osScaleMul(7) - osScaleMul(legends[i].getHeight(null) / 2))), null); + g.setTransform(currentTransform); Renderer.drawShadowText( - g, legendText[i], x + 18, y + offsetY + 12, Renderer.color_text, false); - offsetY += 16; + g, + legendText[i], + x + osScaleMul(18), + y + offsetY + osScaleMul(12), + Renderer.color_text, + false); + offsetY += osScaleMul(16); if (offsetY >= maxY) { - x += 150; + x += osScaleMul(150); offsetY = 0; } } } else { - legendBounds.height = 24; - legendBounds.width = 150; + legendBounds.height = osScaleMul(24); + legendBounds.width = osScaleMul(150); legendBounds.x = canvasWidth - legendBounds.width - BORDER_SIZE; legendBounds.y = BORDER_SIZE; drawButton(g, showLegend ? "" : "Legend", legendBounds); diff --git a/src/Game/Client.java b/src/Game/Client.java index 79f28d1..7784353 100644 --- a/src/Game/Client.java +++ b/src/Game/Client.java @@ -22,6 +22,7 @@ import Client.Logger; import Client.NotificationsHandler; import Client.NotificationsHandler.NotifType; +import Client.ScaledWindow; import Client.Settings; import Client.TwitchIRC; import Client.Util; @@ -245,7 +246,7 @@ public static void init() { } /** - * An updater that runs frequently to update calculations for XP/fatigue drops, the XP bar, etc. + * An updater that runs frequently to update calculations for XP drops, the XP bar, etc. * *

This updater does not handle any rendering, for rendering see {@link Renderer#present} */ @@ -263,24 +264,30 @@ public static void update() { /*if (Settings.RECORD_AUTOMATICALLY_FIRST_TIME.get(Settings.currentProfile) && showRecordAlwaysDialogue) { + + String confirmDefaultRecordMessage = + "If you'd like, you can record your session every time you play by default.
" + + "
" + + "These recordings do not leave your computer unless you manually do it on purpose.
" + + "They also take up negligible space. You could fit about a 6 hour session on a floppy disk, depending on what you do.
" + + "
" + + "Recordings can be played back later, even offline, and capture the data the server sends and that you send the server.
" + + "Your password is not in the capture.
" + + "
" + + "Would you like to record all your play sessions by default?
" + + "
" + + "NOTE: This option can be toggled in the Settings interface (ctrl-o by default) under the Replay tab."; + + JPanel confirmDefaultRecordPanel = Util.createOptionMessagePanel(confirmDefaultRecordMessage); + int response = JOptionPane.showConfirmDialog( Game.getInstance().getApplet(), - "If you'd like, you can record your session every time you play by default.\n" - + "\n" - + "These recordings do not leave your computer unless you manually do it on purpose.\n" - + "They also take up negligible space. You could fit about a 6 hour session on a floppy disk, depending on what you do.\n" - + "\n" - + "Recordings can be played back later, even offline, and capture the data the server sends and that you send the server.\n" - + "Your password is not in the capture.\n" - + "\n" - + "Would you like to record all your play sessions by default?\n" - + "\n" - + "NOTE: This option can be toggled in the Settings interface (ctrl-o by default) under the Replay tab.", + confirmDefaultRecordPanel, "rscplus", JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE, - Launcher.icon); + Launcher.scaled_option_icon); if (response == JOptionPane.YES_OPTION || response == JOptionPane.CLOSED_OPTION) { Settings.RECORD_AUTOMATICALLY.put(Settings.currentProfile, true); } else if (response == JOptionPane.NO_OPTION) { @@ -408,7 +415,7 @@ public static void checkForUpdate(boolean announceIfUpToDate) { MESSAGE_QUEST); if (Settings.CHECK_UPDATES.get(Settings.currentProfile)) { displayMessage( - "~034~ You will receive the update next time you restart rsctimes", MESSAGE_QUEST); + "~034~ You will receive the update next time you restart RSCTimes", MESSAGE_QUEST); } } else if (announceIfUpToDate) { displayMessage( @@ -529,6 +536,10 @@ public static void check_init() { resetXPDrops(false); + // Re-validate the current scaling upon logging in, in case something + // went wrong during the initial window creation and resizing. + ScaledWindow.getInstance().validateAppletSize(); + justLoggedIn = false; } } @@ -751,13 +762,8 @@ public static void messageHook(String inMessage, int messageType) { if (messageType == MESSAGE_GAME) { username = null; type = CHAT_NONE; - if (message.contains("The spell fails! You may try again in 20 seconds")) + if (message.contains("The spell fails! You may try again in 20 seconds")) { magic_timer = Renderer.time + 21000L; - else if (Settings.TRAY_NOTIFS.get(Settings.currentProfile) - && message.contains( - "You have been standing here for 5 mins! Please move to a new area")) { - NotificationsHandler.notify( - NotifType.LOGOUT, "Logout Notification", "You're about to log out"); } // while the message is really You @gr2@are @gr1@poisioned! @gr2@You @gr3@lose @gr2@3 // @gr1@health. @@ -772,26 +778,24 @@ else if (message.contains("poisioned!")) { && is_poisoned) { is_poisoned = false; poison_timer = Renderer.time; + } else if (message.contains("You are under attack!")) { + NotificationsHandler.notify(NotifType.UNDER_ATTACK, "PVP", message); + } else if (message.contains(" wishes to trade with you")) { + type = CHAT_TRADE_REQUEST_RECEIVED; + NotificationsHandler.notify( + NotifType.TRADE, "Trade Request", message.replaceAll("@...@", "")); } } else if (messageType == MESSAGE_PRIVATE) { // should extract sender/receiver here if (message.matches("^(?:@pri@|)You tell.*$")) { type = CHAT_PRIVATE_OUTGOING; NotificationsHandler.notify(NotifType.PM, "PM sent", message); - } else { + } else if (message.matches("^(?:@pri@|).*tells you.*$")) { type = CHAT_PRIVATE; NotificationsHandler.notify(NotifType.PM, "PM received", message); } } else if (messageType == MESSAGE_INVENTORY) { - if (message.contains(" wishes to duel with you")) { - type = CHAT_OTHER; - NotificationsHandler.notify( - NotifType.DUEL, "Duel Request", message.replaceAll("@...@", "")); - } else if (message.contains(" wishes to trade with you")) { - type = CHAT_TRADE_REQUEST_RECEIVED; - NotificationsHandler.notify( - NotifType.TRADE, "Trade Request", message.replaceAll("@...@", "")); - } + // TODO: Nothing here at the moment } else if (messageType == MESSAGE_QUEST) { type = CHAT_QUEST; } else if (messageType == MESSAGE_CHAT) { @@ -1114,6 +1118,15 @@ private static String processClientCommand(String line) { String[] commandArray = line.substring(2, line.length()).toLowerCase().split(" "); switch (commandArray[0]) { + case "togglescaling": + Settings.toggleWindowScaling(); + break; + case "scaleup": + Settings.increaseScale(); + break; + case "scaledown": + Settings.decreaseScale(); + break; /*case "togglebypassattack": Settings.toggleAttackAlwaysLeftClick(); break; diff --git a/src/Game/Game.java b/src/Game/Game.java index 2818660..098f737 100644 --- a/src/Game/Game.java +++ b/src/Game/Game.java @@ -22,9 +22,9 @@ import Client.Launcher; import Client.Logger; import Client.NotificationsHandler; +import Client.ScaledWindow; import Client.Settings; import Client.TrayHandler; -import Client.Util; import Client.WorldMapWindow; import java.applet.Applet; import java.applet.AppletContext; @@ -35,9 +35,20 @@ import java.net.URL; import javax.swing.JFrame; -/** Singleton class that handles packaging the client into a JFrame and starting the applet. */ -public class Game extends JFrame - implements AppletStub, ComponentListener, WindowListener, FocusListener { +/** + * Singleton class that handles packaging the client into a JFrame and starting the applet.
+ *
+ * This frame is never shown to the user, as all graphics rendered by the applet are displayed + * within {@link ScaledWindow}, which in turn forwards all user input and window management events + * back to this class, such that they may be received by the applet.
+ *
+ * This design is necessary due to the fact that the applet performs rendering calculations based on + * the current window size, so in order to accommodate client scaling, we must trick the applet into + * thinking it is always at a fixed window size. Resizes of the {@link ScaledWindow} will tell the + * applet to resize itself, which will actually render outside the window bounds of the {@link Game} + * window, though it is acceptable to do so, as the {@link Game} window is never shown to the user. + */ +public class Game extends JFrame implements AppletStub, ComponentListener, WindowListener { // Singleton private static Game instance = null; @@ -64,11 +75,9 @@ public void start() { if (m_applet == null) return; // m_applet.addMouseListener(new MouseHandler()); - // Set window icon - setIconImage(Launcher.icon.getImage()); // Set window properties - setResizable(true); + setResizable(true); // Must be enabled to accommodate the current loading behavior addWindowListener(this); setMinimumSize(new Dimension(1, 1)); @@ -79,22 +88,19 @@ public void start() { addComponentListener(this); pack(); - // Hide cursor if software cursor - Settings.checkSoftwareCursor(true); - - // Position window and make it visible + // Position window, but do not display it, + // as all graphics from the applet are + // forwarded to the ScaledWindow class setLocationRelativeTo(null); - setVisible(true); + setVisible(false); + + // Launch delegated rendering window + ScaledWindow.getInstance().launchScaledWindow(); updateTitle(); Reflection.Load(); - Game.getInstance().resizeFrameWithContents(); Renderer.init(); - - if (!Util.isMacOS() && Settings.CUSTOM_CLIENT_SIZE.get(Settings.currentProfile)) { - Game.getInstance().resizeFrameWithContents(); - } } public JConfig getJConfig() { @@ -103,13 +109,12 @@ public JConfig getJConfig() { /** Starts the game applet. */ public void launchGame() { - m_applet.addFocusListener(this); m_applet.init(); m_applet.start(); } public void updateTitle() { - String title = "rsctimes ("; + String title = "RSCTimes ("; if (!Replay.isPlaying) { title += Settings.WORLD_NAMES.get(Settings.WORLD.get(Settings.currentProfile)); @@ -138,22 +143,8 @@ public void updateTitle() { } m_title = title; super.setTitle(m_title); - } - /* - * FocusListener methods - */ - - @Override - public final void focusGained(FocusEvent e) {} - - @Override - public final void focusLost(FocusEvent e) { - KeyboardHandler.keyUp = false; - KeyboardHandler.keyDown = false; - KeyboardHandler.keyLeft = false; - KeyboardHandler.keyRight = false; - KeyboardHandler.keyShift = false; + ScaledWindow.getInstance().setTitle(m_title); } /* @@ -200,6 +191,7 @@ public final void windowClosed(WindowEvent e) { @Override public final void windowClosing(WindowEvent e) { dispose(); + Launcher.getScaledWindow().disposeJFrame(); Launcher.getConfigWindow().disposeJFrame(); WorldMapWindow.disposeJFrame(); TrayHandler.removeTrayIcon(); @@ -241,17 +233,7 @@ public final void componentResized(ComponentEvent e) { if (getMinimumSize().width == 1) { setMinimumSize(getSize()); launchGame(); - - // This workaround appears to be for a bug in the macOS JVM - // Without it, mac users get very angry - if (Util.isMacOS()) { - setExtendedState(getExtendedState() | JFrame.MAXIMIZED_BOTH); - setLocationRelativeTo(null); - } } - - Renderer.resize( - getContentPane().getWidth(), getContentPane().getHeight() - Renderer.GAME_RENDER_OFFSET); } @Override @@ -270,23 +252,4 @@ public static Game getInstance() { } return instance; } - - /** - * Resizes the Game window to match the X and Y values stored in Settings. The applet's size will - * be recalculated on the next rendering tick. - */ - public void resizeFrameWithContents() { - int windowWidth = - Settings.CUSTOM_CLIENT_SIZE_X.get(Settings.currentProfile) - + getInsets().left - + getInsets().right; - int windowHeight = - Settings.CUSTOM_CLIENT_SIZE_Y.get(Settings.currentProfile) - + getInsets().top - + getInsets().bottom; - // int windowWidth = 512; - // int windowHeight = 396;//357; - setSize(windowWidth, windowHeight); - setLocationRelativeTo(null); - } } diff --git a/src/Game/MouseHandler.java b/src/Game/MouseHandler.java index 3f3d741..1164463 100644 --- a/src/Game/MouseHandler.java +++ b/src/Game/MouseHandler.java @@ -18,6 +18,7 @@ */ package Game; +import Client.ScaledWindow; import Client.Settings; import Client.WikiURL; import java.awt.Event; @@ -36,7 +37,6 @@ public class MouseHandler implements MouseListener, MouseMotionListener, MouseWh public static int x = 0; public static int y = 0; - public static boolean mouseClicked = false; public static boolean rightClick = false; public static long lastWheelMovement = 0L; public static long lastWheelClick = 0L; @@ -55,7 +55,58 @@ public static boolean inBounds(Rectangle bounds) { } public boolean inConsumableButton() { - return WikiURL.shouldConsume(); + return XPBar.shouldConsume() || WikiURL.shouldConsume(); + } + + /** + * POJO responsible for storing properties about a mouse click, to be used for tracking clicks on + * custom elements introduced by RSCx + */ + public static class BufferedMouseClick { + private final boolean mouseClicked; + private final boolean rightClick; + private final int x; + private final int y; + + public BufferedMouseClick(boolean mouseClicked, boolean rightClick, int x, int y) { + this.mouseClicked = mouseClicked; + this.rightClick = rightClick; + this.x = x; + this.y = y; + } + + public boolean isMouseClicked() { + return mouseClicked; + } + + public boolean isRightClick() { + return rightClick; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + } + + /** + * Grabs a {@link BufferedMouseClick} from the buffer, if available
+ *
+ * Note: this should only ever be called in one place to guarantee queue integrity + */ + public static BufferedMouseClick getBufferedMouseClick() { + BufferedMouseClick bufferedMouseClick = ScaledWindow.getInstance().getInputBuffer().peek(); + + // Return a dummy instance when nothing is available in the queue, such + // that we don't need to do null checks everywhere on the object. + if (bufferedMouseClick == null) { + return new BufferedMouseClick(false, false, -1, -1); + } + + return ScaledWindow.getInstance().getInputBuffer().poll(); } @Override @@ -73,6 +124,9 @@ public void mouseClicked(MouseEvent e) { @Override public void mouseEntered(MouseEvent e) { + // Re-render the software cursor + Settings.checkSoftwareCursor(); + if (!e.isConsumed()) { x = e.getX(); y = e.getY(); @@ -116,8 +170,10 @@ public void mousePressed(MouseEvent e) { e.consume(); } - mouseClicked = true; + // TODO: Determine if required rightClick = SwingUtilities.isRightMouseButton(e); + + ScaledWindow.getInstance().getInputBuffer().add(new BufferedMouseClick(true, rightClick, x, y)); } @Override @@ -195,9 +251,18 @@ public void mouseMoved(MouseEvent e) { public void mouseWheelMoved(MouseWheelEvent e) { x = e.getX(); y = e.getY(); - if (e.getWhen() > lastWheelMovement + 10) { + if ((e.getWhen() > lastWheelMovement + 10) + && !(e.isShiftDown() + && Settings.SHIFT_SCROLL_CAMERA_ROTATION.get(Settings.currentProfile))) { lastWheelMovement = e.getWhen(); Camera.addZoom(e.getWheelRotation() * 16); + } else if (e.isShiftDown() + && Settings.SHIFT_SCROLL_CAMERA_ROTATION.get(Settings.currentProfile)) { + // Allows compatible trackpads to rotate the camera on 2-finger swipe + // motions. Also rotates the camera when holding shift in general. + Camera.addRotation( + e.getWheelRotation() + * -(Settings.TRACKPAD_ROTATION_SENSITIVITY.get(Settings.currentProfile) / 2.0f)); } e.consume(); } diff --git a/src/Game/Renderer.java b/src/Game/Renderer.java index 11c95d9..0516267 100644 --- a/src/Game/Renderer.java +++ b/src/Game/Renderer.java @@ -22,14 +22,17 @@ import Client.Logger; import Client.NotificationsHandler; import Client.NotificationsHandler.NotifType; +import Client.ScaledWindow; import Client.Settings; import Client.Util; import Client.WikiURL; import Client.WorldMapWindow; +import Game.MouseHandler.BufferedMouseClick; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; +import java.awt.Frame; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GraphicsEnvironment; @@ -39,6 +42,7 @@ import java.awt.RenderingHints; import java.awt.font.FontRenderContext; import java.awt.geom.Rectangle2D; +import java.awt.image.AffineTransformOp; import java.awt.image.BufferedImage; import java.awt.image.ImageConsumer; import java.io.File; @@ -49,7 +53,9 @@ import java.util.Comparator; import java.util.Date; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; +import java.util.Queue; import javax.imageio.ImageIO; /** Handles rendering overlays and client adjustments based on window size */ @@ -93,10 +99,17 @@ public class Renderer { public static Image image_bar_frame; public static Image image_bar_frame_short; public static Image image_cursor; + public static Image image_gear; + public static Image image_gear_gold; public static Image image_highlighted_item; public static Image image_wiki_hbar_inactive; public static Image image_wiki_hbar_active; private static BufferedImage game_image; + public static int imageType; + public static float renderingScalar; + public static final float minScalar = 1.0f; + public static final float maxIntegerScalar = 15.0f; + public static final float maxInterpolationScalar = 5.0f; private static Dimension new_size = new Dimension(0, 0); @@ -107,7 +120,8 @@ public class Renderer { private static int frames = 0; private static long fps_timer = 0; - private static boolean screenshot = false; + // Note: this should only ever be called in one place to guarantee queue integrity + private static final Queue screenshotFlagBuffer = new LinkedList<>(); public static boolean combat_menu_shown = false; @@ -134,15 +148,17 @@ public class Renderer { private static int sleepTimer = 0; private static boolean lastInterlace = false; - public static boolean displayingSoftwareCursor = false; - public static boolean lastDisplayingSoftwareCursor = false; public static void init() { // Resize game window new_size.width = 512; new_size.height = 357 - GAME_RENDER_OFFSET; // actual usable height for game content rendering + handle_resize(); + // Set the rendering scalar according to the user's options + updateRenderingScalarAndResize(true); + // Load fonts try { GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); @@ -170,11 +186,70 @@ public static void init() { ImageIO.read(Launcher.getResource("/assets/hbar/wiki_hbar_active.png")); image_cursor = ImageIO.read(Launcher.getResource("/assets/cursor.png")); image_highlighted_item = ImageIO.read(Launcher.getResource("/assets/highlighted_item.png")); + image_gear = ImageIO.read(Launcher.getResource("/assets/gear.png")); + image_gear_gold = ImageIO.read(Launcher.getResource("/assets/gear_gold.png")); } catch (Exception e) { e.printStackTrace(); } } + /** Updates the rendering scalar and resizes the window accordingly */ + private static void updateRenderingScalarAndResize(boolean onInit) { + float scalar = getRenderingScalar(); + imageType = getImageType(); + + // Reset the game image with the current type to ensure that affineOp + // scaling will always have matching source and destination types + game_image = new BufferedImage(width, height + GAME_RENDER_OFFSET, imageType); + + if (scalar != renderingScalar) { + // Handle rendering scalar value changes + renderingScalar = scalar; + + // Only reset the custom window size spinners after initial loading; + // whatever was in the settings file before exiting last time can be + // assumed to be valid on startup + if (!onInit) { + ScaledWindow.getInstance().updateCustomWindowSizeFromSettings(); + } + + // Resize window only after it has begun rendering the game image, + // (ie. not the loading screen) + if (ScaledWindow.getInstance().isViewportLoaded()) { + ScaledWindow.getInstance().resizeWindowToScalar(); + } + } else { + // Handle window size changes when the scalar did not change, + // typically due to settings window changes + if (Settings.CUSTOM_CLIENT_SIZE.get(Settings.currentProfile)) { + int customClientWidth = Settings.CUSTOM_CLIENT_SIZE_X.get(Settings.currentProfile); + int customClientHeight = Settings.CUSTOM_CLIENT_SIZE_Y.get(Settings.currentProfile); + + Dimension maxWindowDimensions = ScaledWindow.getInstance().getMaximumEffectiveWindowSize(); + int maxWindowWidth = maxWindowDimensions.width; + int maxWindowHeight = maxWindowDimensions.height; + + // If window is maximized in the Windows OS sense and custom size matches + // max window boundaries, don't resize or realign the window + if (ScaledWindow.getInstance().getExtendedState() == Frame.MAXIMIZED_BOTH + && customClientWidth == maxWindowWidth + && customClientHeight == maxWindowHeight) { + return; + } + + int frameWidth = customClientWidth + ScaledWindow.getInstance().getWindowWidthInsets(); + int frameHeight = customClientHeight + ScaledWindow.getInstance().getWindowHeightInsets(); + + // Only set the size and align the window if the window actually changed sizes + if (ScaledWindow.getInstance().getWidth() != frameWidth + || ScaledWindow.getInstance().getHeight() != frameHeight) { + ScaledWindow.getInstance().setWindowRealignmentIntent(true); + ScaledWindow.getInstance().setSize(new Dimension(frameWidth, frameHeight)); + } + } + } + } + public static void resize(int w, int h) { new_size.width = Math.max(w, 512); new_size.height = @@ -188,7 +263,8 @@ public static void handle_resize() { height_client = height - 12; pixels = new int[width * height]; - game_image = new BufferedImage(width, height + GAME_RENDER_OFFSET, BufferedImage.TYPE_INT_ARGB); + + game_image = new BufferedImage(width, height + GAME_RENDER_OFFSET, getImageType()); Camera.resize(); Menu.resize(); @@ -204,28 +280,69 @@ public static void handle_resize() { } } - public static void present(Graphics g, Image image) { + /** + * Grab the scaling algorithm from the settings, defaulting to {@link BufferedImage#TYPE_INT_RGB} + */ + private static int getImageType() { + if (Settings.SCALED_CLIENT_WINDOW.get(Settings.currentProfile)) { + if (Settings.SCALING_ALGORITHM + .get(Settings.currentProfile) + .equals(AffineTransformOp.TYPE_NEAREST_NEIGHBOR)) { + return BufferedImage.TYPE_INT_RGB; + } else if (Settings.SCALING_ALGORITHM + .get(Settings.currentProfile) + .equals(AffineTransformOp.TYPE_BILINEAR)) { + return BufferedImage.TYPE_3BYTE_BGR; + } else if (Settings.SCALING_ALGORITHM + .get(Settings.currentProfile) + .equals(AffineTransformOp.TYPE_BICUBIC)) { + return BufferedImage.TYPE_3BYTE_BGR; + } + } + + return BufferedImage.TYPE_INT_RGB; + } + + /** + * Grab the rendering scalar from the settings, depending on the chosen scaling algorithm. + * Defaults to {@code 1.0f} + */ + private static float getRenderingScalar() { + if (Settings.SCALED_CLIENT_WINDOW.get(Settings.currentProfile)) { + if (Settings.SCALING_ALGORITHM + .get(Settings.currentProfile) + .equals(AffineTransformOp.TYPE_NEAREST_NEIGHBOR)) { + return Settings.INTEGER_SCALING_FACTOR.get(Settings.currentProfile); + } else if (Settings.SCALING_ALGORITHM + .get(Settings.currentProfile) + .equals(AffineTransformOp.TYPE_BILINEAR)) { + return Settings.BILINEAR_SCALING_FACTOR.get(Settings.currentProfile); + } else if (Settings.SCALING_ALGORITHM + .get(Settings.currentProfile) + .equals(AffineTransformOp.TYPE_BICUBIC)) { + return Settings.BICUBIC_SCALING_FACTOR.get(Settings.currentProfile); + } + } + + return 1.0f; + } + + public static void present(Image image) { // Update timing long new_time = System.currentTimeMillis(); delta_time = (float) (new_time - time) / 1000.0f; time = new_time; alpha_time = 0.25f + (((float) Math.sin(time / 100) + 1.0f) / 2.0f * 0.75f); - // This workaround is required to use custom resolution on macOS - if (macOS_resize_workaround) { - if (Settings.CUSTOM_CLIENT_SIZE.get(Settings.currentProfile)) { - Game.getInstance().resizeFrameWithContents(); - } else { - Game.getInstance().pack(); - Game.getInstance().setLocationRelativeTo(null); - } - macOS_resize_workaround = false; - } - // Reset dialogue option after force pressed in replay if (Replay.isPlaying && KeyboardHandler.dialogue_option != -1) KeyboardHandler.dialogue_option = -1; + if (Settings.renderingScalarUpdateRequired) { + updateRenderingScalarAndResize(false); + Settings.renderingScalarUpdateRequired = false; + } + Graphics2D g2 = (Graphics2D) game_image.getGraphics(); // TODO: Declare g2 outside of the present method g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); @@ -235,7 +352,10 @@ public static void present(Graphics g, Image image) { int startingPixel = 512; // int startingPixel = Settings.PATCH_HBAR_512_LAST_PIXEL.get(Settings.currentProfile) ? 511 : // 512; - g2.drawImage(image_border_top, startingPixel, 0, width - startingPixel, 11, null); + + // After introduction of scaling, we are now drawing the entire top border as an overlay + // due to the discrepancy in how the top border is drawn in mudclient + g2.drawImage(image_border_top, 0, 0, width, 11, null); g2.drawImage( image_border, startingPixel, @@ -245,6 +365,13 @@ public static void present(Graphics g, Image image) { null); // In-game UI + + // Attempt to grab a buffered mouse click + BufferedMouseClick bufferedMouseClick = MouseHandler.getBufferedMouseClick(); + + // Grab the screenshot intent from the buffer + boolean screenshot = getScreenshotFlag(); + if (Client.state == Client.STATE_GAME) { int npcCount = 0; int playerCount = 0; @@ -585,7 +712,7 @@ public static void present(Graphics g, Image image) { Client.npc_list_retained = new ArrayList(Client.npc_list); Client.npc_list.clear(); - Client.xpbar.draw(g2); + Client.xpbar.draw(g2, bufferedMouseClick); // World Map // Arrow marker for destination @@ -612,8 +739,8 @@ public static void present(Graphics g, Image image) { } // RSC Wiki integration - if (WikiURL.nextClickIsLookup && MouseHandler.mouseClicked) { - if (MouseHandler.rightClick) { + if (WikiURL.nextClickIsLookup && bufferedMouseClick.isMouseClicked()) { + if (bufferedMouseClick.isRightClick()) { WikiURL.nextClickIsLookup = false; Client.displayMessage("Cancelled lookup.", Client.CHAT_NONE); } else { @@ -645,12 +772,12 @@ public static void present(Graphics g, Image image) { if (Settings.WIKI_LOOKUP_ON_HBAR.get(Settings.currentProfile)) { int xCoord = 410; int yCoord = height + GAME_RENDER_OFFSET - 16; - // Handle replay play selection click - if (MouseHandler.x >= xCoord + 3 // + 3 for hbar shadow included in image - && MouseHandler.x <= xCoord + 3 + 90 - && MouseHandler.y >= height + GAME_RENDER_OFFSET - 16 - && MouseHandler.y <= height + GAME_RENDER_OFFSET - && MouseHandler.mouseClicked) { + // Handle wiki lookup click + if (bufferedMouseClick.getX() >= xCoord + 3 // + 3 for hbar shadow included in image + && bufferedMouseClick.getX() <= xCoord + 3 + 90 + && bufferedMouseClick.getY() >= height + GAME_RENDER_OFFSET - 16 + && bufferedMouseClick.getY() <= height + GAME_RENDER_OFFSET + && bufferedMouseClick.isMouseClicked()) { Client.displayMessage( "Click on something to look it up on the wiki...", Client.CHAT_NONE); WikiURL.nextClickIsLookup = true; @@ -663,9 +790,10 @@ public static void present(Graphics g, Image image) { } // Interface rscx buttons - // Map Button if (Settings.RSCTIMES_BUTTONS_FUNCTIONAL.get(Settings.currentProfile) || Settings.SHOW_RSCTIMES_BUTTONS.get(Settings.currentProfile)) { + + // Map Button Rectangle mapButtonBounds = new Rectangle(width - 68, 3 + GAME_RENDER_OFFSET, 32, 32); if (Settings.SHOW_RSCTIMES_BUTTONS.get(Settings.currentProfile)) { g2.setColor(Renderer.color_text); @@ -681,14 +809,14 @@ public static void present(Graphics g, Image image) { mapButtonBounds.y + 7); } - // Handle replay play selection click - if (MouseHandler.x >= mapButtonBounds.x - && MouseHandler.x <= mapButtonBounds.x + mapButtonBounds.width - && MouseHandler.y >= mapButtonBounds.y - && MouseHandler.y <= mapButtonBounds.y + mapButtonBounds.height - && MouseHandler.mouseClicked) { + // Handle map button click + if (bufferedMouseClick.getX() >= mapButtonBounds.x + && bufferedMouseClick.getX() <= mapButtonBounds.x + mapButtonBounds.width + && bufferedMouseClick.getY() >= mapButtonBounds.y + && bufferedMouseClick.getY() <= mapButtonBounds.y + mapButtonBounds.height + && bufferedMouseClick.isMouseClicked()) { - Launcher.getWorldMapWindow().showWorldMapWindow(); + Launcher.getWorldMapWindow().toggleWorldMapWindow(); } // Settings @@ -707,14 +835,14 @@ public static void present(Graphics g, Image image) { mapButtonBounds.y + 7); } - // Handle replay play selection click - if (MouseHandler.x >= mapButtonBounds.x - && MouseHandler.x <= mapButtonBounds.x + mapButtonBounds.width - && MouseHandler.y >= mapButtonBounds.y - && MouseHandler.y <= mapButtonBounds.y + mapButtonBounds.height - && MouseHandler.mouseClicked) { + // Handle settings button click + if (bufferedMouseClick.getX() >= mapButtonBounds.x + && bufferedMouseClick.getX() <= mapButtonBounds.x + mapButtonBounds.width + && bufferedMouseClick.getY() >= mapButtonBounds.y + && bufferedMouseClick.getY() <= mapButtonBounds.y + mapButtonBounds.height + && bufferedMouseClick.isMouseClicked()) { - Launcher.getConfigWindow().showConfigWindow(); + Launcher.getConfigWindow().toggleConfigWindow(); } // wiki button on magic book (Probably a bad idea due to misclicks) @@ -734,12 +862,12 @@ public static void present(Graphics g, Image image) { mapButtonBounds.y + 7); } - // Handle replay play selection click - if (MouseHandler.x >= mapButtonBounds.x - && MouseHandler.x <= mapButtonBounds.x + mapButtonBounds.width - && MouseHandler.y >= mapButtonBounds.y - && MouseHandler.y <= mapButtonBounds.y + mapButtonBounds.height - && MouseHandler.mouseClicked) { + // Handle magic book button click + if (bufferedMouseClick.getX() >= mapButtonBounds.x + && bufferedMouseClick.getX() <= mapButtonBounds.x + mapButtonBounds.width + && bufferedMouseClick.getY() >= mapButtonBounds.y + && bufferedMouseClick.getY() <= mapButtonBounds.y + mapButtonBounds.height + && bufferedMouseClick.isMouseClicked()) { Client.displayMessage( "Click on something to look it up on the wiki...", Client.CHAT_NONE); WikiURL.nextClickIsLookup = true; @@ -764,13 +892,13 @@ public static void present(Graphics g, Image image) { mapButtonBounds.y + 7); } - // Handle replay play selection click - if (MouseHandler.x >= mapButtonBounds.x - && MouseHandler.x <= mapButtonBounds.x + mapButtonBounds.width - && MouseHandler.y >= mapButtonBounds.y - && MouseHandler.y <= mapButtonBounds.y + mapButtonBounds.height - && MouseHandler.mouseClicked) { - if (MouseHandler.rightClick) { + // Handle friends button click + if (bufferedMouseClick.getX() >= mapButtonBounds.x + && bufferedMouseClick.getX() <= mapButtonBounds.x + mapButtonBounds.width + && bufferedMouseClick.getY() >= mapButtonBounds.y + && bufferedMouseClick.getY() <= mapButtonBounds.y + mapButtonBounds.height + && bufferedMouseClick.isMouseClicked()) { + if (bufferedMouseClick.isRightClick()) { if (Settings.MOTIVATIONAL_QUOTES_BUTTON.get(Settings.currentProfile)) { Client.displayMotivationalQuote(); } @@ -813,13 +941,13 @@ public static void present(Graphics g, Image image) { mapButtonBounds.y + 7); } - // Handle replay play selection click - if (MouseHandler.x >= mapButtonBounds.x - && MouseHandler.x <= mapButtonBounds.x + mapButtonBounds.width - && MouseHandler.y >= mapButtonBounds.y - && MouseHandler.y <= mapButtonBounds.y + mapButtonBounds.height - && MouseHandler.mouseClicked) { - if (MouseHandler.rightClick) { + // Handle stats menu click + if (bufferedMouseClick.getX() >= mapButtonBounds.x + && bufferedMouseClick.getX() <= mapButtonBounds.x + mapButtonBounds.width + && bufferedMouseClick.getY() >= mapButtonBounds.y + && bufferedMouseClick.getY() <= mapButtonBounds.y + mapButtonBounds.height + && bufferedMouseClick.isMouseClicked()) { + if (bufferedMouseClick.isRightClick()) { Settings.toggleGoalBar(); } else { Settings.toggleGoalBarPin(); @@ -829,7 +957,7 @@ public static void present(Graphics g, Image image) { } // Handle setting XP Bar stat from STATS menu - if (Client.show_menu == Client.MENU_STATS && MouseHandler.mouseClicked) { + if (Client.show_menu == Client.MENU_STATS && bufferedMouseClick.isMouseClicked()) { int xOffset = width - 199; int yOffset = 65; boolean clickedSkill = false; @@ -837,19 +965,19 @@ public static void present(Graphics g, Image image) { for (int skillIdx = 0; skillIdx < 10; ++skillIdx) { // Column 1 if (skillIdx < 9 - && MouseHandler.x > xOffset + 3 - && MouseHandler.x < xOffset + 90 - && MouseHandler.y >= yOffset - 6 - && MouseHandler.y < yOffset + 6) { + && bufferedMouseClick.getX() > xOffset + 3 + && bufferedMouseClick.getX() < xOffset + 90 + && bufferedMouseClick.getY() >= yOffset - 6 + && bufferedMouseClick.getY() < yOffset + 6) { selectedSkill = skillIdx; clickedSkill = true; break; } // Column 2 - if (MouseHandler.x >= xOffset + 90 - && MouseHandler.x < xOffset + 196 - && MouseHandler.y >= yOffset - 18 - && MouseHandler.y < yOffset - 6) { + if (bufferedMouseClick.getX() >= xOffset + 90 + && bufferedMouseClick.getX() < xOffset + 196 + && bufferedMouseClick.getY() >= yOffset - 18 + && bufferedMouseClick.getY() < yOffset - 6) { selectedSkill = skillIdx + 9; clickedSkill = true; break; @@ -1119,6 +1247,32 @@ public static void present(Graphics g, Image image) { if (Settings.DEBUG.get(Settings.currentProfile)) drawShadowText(g2, "DEBUG MODE", 38, 8, color_text, true); + // Draw settings gear + int gearX = width - 10 - image_gear.getWidth(null); + int gearY = 19; + + Rectangle gearBounds = + new Rectangle(gearX, gearY, image_gear.getWidth(null), image_gear.getHeight(null)); + + // Draw gold version on hover, grey otherwise + if (MouseHandler.x >= gearBounds.x + && MouseHandler.x <= gearBounds.x + gearBounds.width + && MouseHandler.y >= gearBounds.y + && MouseHandler.y <= gearBounds.y + gearBounds.height) { + g2.drawImage(image_gear_gold, gearX, gearY, null); + } else { + g2.drawImage(image_gear, gearX, gearY, null); + } + + // Handle gear click + if (bufferedMouseClick.getX() >= gearBounds.x + && bufferedMouseClick.getX() <= gearBounds.x + gearBounds.width + && bufferedMouseClick.getY() >= gearBounds.y + && bufferedMouseClick.getY() <= gearBounds.y + gearBounds.height + && bufferedMouseClick.isMouseClicked()) { + Launcher.getConfigWindow().toggleConfigWindow(); + } + // Draw world list drawShadowText( g2, "World (Click to change): ", 80, height + GAME_RENDER_OFFSET - 8, color_text, true); @@ -1137,11 +1291,11 @@ public static void present(Graphics g, Image image) { g2, worldString, bounds.x + (bounds.width / 2), bounds.y + 4, color_text, true); // Handle world selection click - if (MouseHandler.x >= bounds.x - && MouseHandler.x <= bounds.x + bounds.width - && MouseHandler.y >= bounds.y - && MouseHandler.y <= bounds.y + bounds.height - && MouseHandler.mouseClicked) { + if (bufferedMouseClick.getX() >= bounds.x + && bufferedMouseClick.getX() <= bounds.x + bounds.width + && bufferedMouseClick.getY() >= bounds.y + && bufferedMouseClick.getY() <= bounds.y + bounds.height + && bufferedMouseClick.isMouseClicked()) { Game.getInstance().getJConfig().changeWorld(i); } } @@ -1257,11 +1411,11 @@ public static void present(Graphics g, Image image) { true); // Handle replay record selection click - if (MouseHandler.x >= recordButtonBounds.x - && MouseHandler.x <= recordButtonBounds.x + recordButtonBounds.width - && MouseHandler.y >= recordButtonBounds.y - && MouseHandler.y <= recordButtonBounds.y + recordButtonBounds.height - && MouseHandler.mouseClicked) { + if (bufferedMouseClick.getX() >= recordButtonBounds.x + && bufferedMouseClick.getX() <= recordButtonBounds.x + recordButtonBounds.width + && bufferedMouseClick.getY() >= recordButtonBounds.y + && bufferedMouseClick.getY() <= recordButtonBounds.y + recordButtonBounds.height + && bufferedMouseClick.isMouseClicked()) { Client.showRecordAlwaysDialogue = true; if (replayOption == 1) { @@ -1300,11 +1454,11 @@ public static void present(Graphics g, Image image) { true); // Handle replay play selection click - /*if (MouseHandler.x >= playButtonBounds.x - && MouseHandler.x <= playButtonBounds.x + playButtonBounds.width - && MouseHandler.y >= playButtonBounds.y - && MouseHandler.y <= playButtonBounds.y + playButtonBounds.height - && MouseHandler.mouseClicked) { + /*if (bufferedMouseClick.getX() >= playButtonBounds.x + && bufferedMouseClick.getX() <= playButtonBounds.x + playButtonBounds.width + && bufferedMouseClick.getY() >= playButtonBounds.y + && bufferedMouseClick.getY() <= playButtonBounds.y + playButtonBounds.height + && bufferedMouseClick.isMouseClicked()) { if (replayOption == 2) { replayOption = 0; } else { @@ -1321,7 +1475,7 @@ public static void present(Graphics g, Image image) { // Draw version information drawShadowText( g2, - "rsctimes v" + String.format("%8.6f", Settings.VERSION_NUMBER), + "RSCTimes v" + String.format("%8.6f", Settings.VERSION_NUMBER), width - 164, height + GAME_RENDER_OFFSET - 2, color_text, @@ -1419,7 +1573,7 @@ public static void present(Graphics g, Image image) { true, 0); - if (!Replay.isSeeking && MouseHandler.mouseClicked) Replay.seek(timestamp); + if (!Replay.isSeeking && bufferedMouseClick.isMouseClicked()) Replay.seek(timestamp); } if (Replay.isSeeking) { @@ -1474,7 +1628,7 @@ public static void present(Graphics g, Image image) { - (int) ((float) shapeHeight / 2.0); drawPlayerControlShape(g2, shapeX, previousBounds.y + 2, shapeHeight, "previous"); - if (MouseHandler.inBounds(previousBounds) && MouseHandler.mouseClicked) + if (MouseHandler.inBounds(previousBounds) && bufferedMouseClick.isMouseClicked()) Replay.controlPlayback("prev"); // slowdown button @@ -1509,7 +1663,7 @@ public static void present(Graphics g, Image image) { - (int) ((float) shapeHeight / 2.0); drawPlayerControlShape(g2, shapeX, slowForwardBounds.y + 2, shapeHeight, "slowforward"); - if (MouseHandler.inBounds(slowForwardBounds) && MouseHandler.mouseClicked) { + if (MouseHandler.inBounds(slowForwardBounds) && bufferedMouseClick.isMouseClicked()) { Replay.controlPlayback("ff_minus"); } @@ -1542,7 +1696,7 @@ public static void present(Graphics g, Image image) { - (int) ((float) shapeHeight / 2.0); drawPlayerControlShape(g2, shapeX, playPauseBounds.y + 2, shapeHeight, "playpause"); - if (MouseHandler.inBounds(playPauseBounds) && MouseHandler.mouseClicked) { + if (MouseHandler.inBounds(playPauseBounds) && bufferedMouseClick.isMouseClicked()) { Replay.togglePause(); } // fastforward button @@ -1577,7 +1731,7 @@ public static void present(Graphics g, Image image) { - (int) ((float) shapeHeight / 2.0); drawPlayerControlShape(g2, shapeX, fastForwardBounds.y + 2, shapeHeight, "fastforward"); - if (MouseHandler.inBounds(fastForwardBounds) && MouseHandler.mouseClicked) { + if (MouseHandler.inBounds(fastForwardBounds) && bufferedMouseClick.isMouseClicked()) { Replay.controlPlayback("ff_plus"); } @@ -1606,7 +1760,7 @@ public static void present(Graphics g, Image image) { - (int) ((float) shapeHeight / 2.0); drawPlayerControlShape(g2, shapeX, nextBounds.y + 2, shapeHeight, "next"); - if (MouseHandler.inBounds(nextBounds) && MouseHandler.mouseClicked) + if (MouseHandler.inBounds(nextBounds) && bufferedMouseClick.isMouseClicked()) Replay.controlPlayback("next"); // open queue button (right aligned) @@ -1639,7 +1793,7 @@ public static void present(Graphics g, Image image) { color_white, false); - if (MouseHandler.inBounds(queueBounds) && MouseHandler.mouseClicked) { + if (MouseHandler.inBounds(queueBounds) && bufferedMouseClick.isMouseClicked()) { Launcher.getQueueWindow().showQueueWindow(); } @@ -1668,7 +1822,7 @@ public static void present(Graphics g, Image image) { - (int) ((float) shapeHeight / 2.0); drawPlayerControlShape(g2, shapeX, stopBounds.y + 2, shapeHeight, "stop"); - if (MouseHandler.inBounds(stopBounds) && MouseHandler.mouseClicked) { + if (MouseHandler.inBounds(stopBounds) && bufferedMouseClick.isMouseClicked()) { Replay.controlPlayback("stop"); } } @@ -1676,19 +1830,16 @@ public static void present(Graphics g, Image image) { } // Draw software cursor - lastDisplayingSoftwareCursor = displayingSoftwareCursor; - displayingSoftwareCursor = false; if (screenshot || Settings.SOFTWARE_CURSOR.get(Settings.currentProfile)) { - if (MouseHandler.y > GAME_RENDER_OFFSET) { - setAlpha(g2, 1.0f); - g2.drawImage(image_cursor, MouseHandler.x, MouseHandler.y, null); - displayingSoftwareCursor = true; - } + setAlpha(g2, 1.0f); + g2.drawImage(image_cursor, MouseHandler.x, MouseHandler.y, null); } - Settings.checkSoftwareCursor(false); g2.dispose(); + // Forward the image to be drawn by ScaledWindow.java + ScaledWindow.getInstance().setGameImage(game_image); + // Right now is a good time to take a screenshot if one is requested if (screenshot) { try { @@ -1702,11 +1853,8 @@ public static void present(Graphics g, Image image) { "@cya@Screenshot saved to '" + screenshotFile.toString() + "'", Client.CHAT_NONE); } catch (Exception e) { } - screenshot = false; } - g.drawImage(game_image, 0, 0, null); - frames++; if (Settings.FPS_LIMIT_ENABLED.get(Settings.currentProfile)) { @@ -1781,14 +1929,14 @@ public static void present(Graphics g, Image image) { } // handle resize - if (width != new_size.width || height != new_size.height) handle_resize(); + if (width != new_size.width || height != new_size.height) { + handle_resize(); + } + if (Settings.fovUpdateRequired) { Camera.setFoV(Settings.FOV.get(Settings.currentProfile)); Settings.fovUpdateRequired = false; } - - // Reset the mouse click handler - MouseHandler.mouseClicked = false; } public static void drawShadowText( @@ -1970,9 +2118,20 @@ public static String getFixedRemoveString(String origString) { return "~" + coord + "~@whi@Remove WWWWWWWWWW"; } + /** Grabs a screenshot intent from the buffer */ + public static boolean getScreenshotFlag() { + if (screenshotFlagBuffer.peek() == null) { + return false; + } + + return screenshotFlagBuffer.poll(); + } + public static void takeScreenshot(boolean quiet) { quietScreenshot = quiet; - screenshot = true; + + // Add a screenshot intent to the buffer + screenshotFlagBuffer.add(true); } private static Dimension getStringBounds(Graphics2D g, String str) { diff --git a/src/Game/XPBar.java b/src/Game/XPBar.java index 2c73415..f1c3dc3 100644 --- a/src/Game/XPBar.java +++ b/src/Game/XPBar.java @@ -21,6 +21,7 @@ import Client.Logger; import Client.Settings; import Client.Util; +import Game.MouseHandler.BufferedMouseClick; import java.awt.*; import java.text.NumberFormat; @@ -69,7 +70,7 @@ void setSkill(int skill) { * * @param g the Graphics2D object */ - void draw(Graphics2D g) { + void draw(Graphics2D g, BufferedMouseClick bufferedMouseClick) { // current_skill from skill not known /*if (Renderer.time > m_timer && !pinnedBar) { @@ -173,7 +174,7 @@ void draw(Graphics2D g) { && showingMenu); if (hoveringOverBar()) { - if (MouseHandler.mouseClicked) { + if (bufferedMouseClick.isRightClick()) { showingMenu = true; } else { if (!showingMenu) { @@ -196,7 +197,7 @@ void draw(Graphics2D g) { } } - if (showingMenu) drawMenu(g, x, y); + if (showingMenu) drawMenu(g, x, y, bufferedMouseClick); Renderer.setAlpha(g, 1.0f); } @@ -209,7 +210,7 @@ static boolean hoveringOverBar() { && alpha > 0.01; } - private void drawMenu(Graphics2D g, int x, int y) { + private void drawMenu(Graphics2D g, int x, int y, BufferedMouseClick bufferedMouseClick) { x = xp_bar_x; y = xp_bar_y + bounds.height; @@ -228,7 +229,7 @@ private void drawMenu(Graphics2D g, int x, int y) { // Option 0 if (MouseHandler.y > y + offset && MouseHandler.y < y + textHeight) { textColour = Renderer.color_yellow; - if (MouseHandler.mouseClicked) { + if (bufferedMouseClick.isMouseClicked()) { setLvlGoal(); } } else { @@ -241,7 +242,7 @@ private void drawMenu(Graphics2D g, int x, int y) { // Option 1 if (MouseHandler.y > y + offset && MouseHandler.y < y + textHeight) { textColour = Renderer.color_yellow; - if (MouseHandler.mouseClicked) { + if (bufferedMouseClick.isMouseClicked()) { togglePinnedBar(); } } else { @@ -360,6 +361,14 @@ public static boolean shouldShowGoalInput() { return drawGoalInputState != 0; } + public static boolean shouldConsume() { + if (XPBar.hoveringOverMenu || XPBar.hoveringOverBar()) { + XPBar.hoveringOverMenu = false; + return true; + } + return false; + } + public static boolean shouldConsumeKey() { return shouldShowGoalInput(); }