diff --git a/src/main/java/brainwine/Bootstrap.java b/src/main/java/brainwine/Bootstrap.java index 836475e..82a3306 100644 --- a/src/main/java/brainwine/Bootstrap.java +++ b/src/main/java/brainwine/Bootstrap.java @@ -89,9 +89,10 @@ public class Bootstrap { UIManager.put("Brainwine.powerIcon", new ImageIcon(getClass().getResource("/powerIcon16x.png"))); UIManager.put("Brainwine.consoleFont", new Font("Consolas", Font.PLAIN, 12)); UIManager.put("Spinner.editorAlignment", JTextField.LEFT); + UIManager.put("TitlePane.unifiedBackground", false); + UIManager.put("Button.foreground", UIManager.get("MenuBar.foreground")); SwingUtils.setDefaultFontSize(Math.min(28, Math.max(10, GuiPreferences.getInt(GuiPreferences.FONT_SIZE_KEY, 14)))); SwingUtils.setMenuBarEmbedded(GuiPreferences.getBoolean(GuiPreferences.EMBED_MENU_BAR_KEY, true)); - GuiPreferences.createPropertyListeners(); // Create view mainView = new MainView(this); diff --git a/src/main/java/brainwine/gui/GamePanel.java b/src/main/java/brainwine/gui/GamePanel.java index 93e1a12..04e4e49 100644 --- a/src/main/java/brainwine/gui/GamePanel.java +++ b/src/main/java/brainwine/gui/GamePanel.java @@ -2,21 +2,32 @@ package brainwine.gui; import static brainwine.gui.GuiConstants.DEEPWORLD_ASSEMBLY_PATH; import static brainwine.gui.GuiConstants.DEEPWORLD_PLAYERPREFS; +import static brainwine.gui.GuiConstants.GUI_MARKER; import static brainwine.gui.GuiConstants.HTTP_COMMUNITY_HUB_URL; import static brainwine.gui.GuiConstants.HTTP_STEAM_DOWNLOAD_URL; import static brainwine.gui.GuiConstants.STEAM_COMMUNITY_HUB_URL; import static brainwine.gui.GuiConstants.STEAM_REGISTRY_LOCATION; import static brainwine.gui.GuiConstants.STEAM_RUN_GAME_URL; +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; import java.awt.GridBagLayout; import java.awt.GridLayout; +import java.awt.LinearGradientPaint; import java.io.File; +import java.io.IOException; +import javax.imageio.ImageIO; import javax.swing.JButton; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.UIManager; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import brainwine.gui.component.ImagePanel; import brainwine.util.DesktopUtils; import brainwine.util.ProcessResult; import brainwine.util.RegistryKey; @@ -24,8 +35,11 @@ import brainwine.util.RegistryUtils; import brainwine.util.SwingUtils; @SuppressWarnings("serial") -public class GamePanel extends JPanel { +public class GamePanel extends ImagePanel { + private static final Logger logger = LogManager.getLogger(); + private final LinearGradientPaint gradientPaint = new LinearGradientPaint(0, 0, 0, 15, + new float[] {0.0F, 1.0F}, new Color[] {Color.BLACK, new Color(0, 0, 0, 0)}); private final JButton startGameButton; private final JButton communityHubButton; @@ -46,12 +60,34 @@ public class GamePanel extends JPanel { // Button panel JPanel buttonPanel = new JPanel(new GridBagLayout()); + buttonPanel.setOpaque(false); + JPanel topPanel = new JPanel(new GridLayout(1, 2)); + topPanel.setOpaque(false); topPanel.add(hostSettingsButton); topPanel.add(communityHubButton); buttonPanel.add(topPanel, SwingUtils.createConstraints(0, 0)); buttonPanel.add(startGameButton, SwingUtils.createConstraints(0, 1, 2, 1)); add(buttonPanel); + + // Load & set background image + try { + setImage(ImageIO.read(getClass().getResourceAsStream("/background.jpg"))); + } catch (IllegalArgumentException | IOException e) { + logger.error(GUI_MARKER, "Could not load background image", e); + } + } + + @Override + public void paintComponent(Graphics graphics) { + super.paintComponent(graphics); + + // Draw shadow gradient below the title bar if this panel has a background image + if(getImage() != null) { + Graphics2D g2d = (Graphics2D)graphics; + g2d.setPaint(gradientPaint); + g2d.fillRect(0, 0, getWidth(), 15); + } } private void startGame() { diff --git a/src/main/java/brainwine/gui/GuiPreferences.java b/src/main/java/brainwine/gui/GuiPreferences.java index bec635e..92d416c 100644 --- a/src/main/java/brainwine/gui/GuiPreferences.java +++ b/src/main/java/brainwine/gui/GuiPreferences.java @@ -1,18 +1,13 @@ package brainwine.gui; -import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; -import javax.swing.UIManager; - -import brainwine.util.SwingUtils; - public class GuiPreferences { public static final String ROOT_KEY = "brainwine"; public static final String THEME_KEY = "theme"; + public static final String TAB_PLACEMENT_KEY = "tabPlacement"; public static final String FONT_SIZE_KEY = "fontSize"; public static final String EMBED_MENU_BAR_KEY = "embedMenuBar"; public static final String GATEWAY_HOST_KEY = "gatewayHost"; @@ -27,29 +22,6 @@ public class GuiPreferences { return preferences; } - public static void createPropertyListeners() { - UIManager.addPropertyChangeListener(new PropertyChangeListener() { - @Override - public void propertyChange(PropertyChangeEvent event) { - if(event.getPropertyName().equals("lookAndFeel")) { - setString(THEME_KEY, UIManager.getLookAndFeel().getClass().getName()); - } - } - }); - - UIManager.getDefaults().addPropertyChangeListener(new PropertyChangeListener() { - @Override - public void propertyChange(PropertyChangeEvent event) { - String name = event.getPropertyName(); - - switch(name) { - case "defaultFont": setInt(FONT_SIZE_KEY, SwingUtils.getDefaultFontSize()); break; - case "TitlePane.menuBarEmbedded": setBoolean(EMBED_MENU_BAR_KEY, SwingUtils.isMenuBarEmbedded()); break; - } - } - }); - } - public static void setString(String key, String value) { get().put(key, value); } diff --git a/src/main/java/brainwine/gui/MainView.java b/src/main/java/brainwine/gui/MainView.java index 0581334..39366e8 100644 --- a/src/main/java/brainwine/gui/MainView.java +++ b/src/main/java/brainwine/gui/MainView.java @@ -2,6 +2,7 @@ package brainwine.gui; import static brainwine.gui.GuiConstants.DEEPWORLD_PLAYERPREFS; import static brainwine.gui.GuiConstants.GITHUB_REPOSITORY_URL; +import static brainwine.gui.GuiConstants.GUI_MARKER; import java.awt.BorderLayout; import java.awt.Dimension; @@ -15,12 +16,12 @@ import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JOptionPane; import javax.swing.JPanel; -import javax.swing.JTabbedPane; import javax.swing.UIManager; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import com.formdev.flatlaf.extras.FlatAnimatedLafChange; import com.formdev.flatlaf.extras.components.FlatTabbedPane; import com.formdev.flatlaf.extras.components.FlatTabbedPane.TabAlignment; @@ -42,25 +43,23 @@ public class MainView { private final SettingsPanel settingsPanel; public MainView(Bootstrap bootstrap) { - logger.info("Creating main view ..."); + logger.info(GUI_MARKER, "Creating main view ..."); - // Panels + // Panel panel = new JPanel(new BorderLayout()); - serverPanel = new ServerPanel(bootstrap); - settingsPanel = new SettingsPanel(); // Tabs tabbedPane = new FlatTabbedPane(); tabbedPane.setShowContentSeparators(true); - tabbedPane.setTabPlacement(JTabbedPane.LEFT); tabbedPane.setTabAlignment(TabAlignment.leading); + setTabPlacement(GuiPreferences.getInt(GuiPreferences.TAB_PLACEMENT_KEY, 1), false); if(OperatingSystem.isWindows()) { tabbedPane.addTab("Play Game", UIManager.getIcon("Brainwine.playIcon"), new GamePanel(this)); } - tabbedPane.addTab("Server", UIManager.getIcon("Brainwine.serverIcon"), serverPanel); - tabbedPane.addTab("Settings", UIManager.getIcon("Brainwine.settingsIcon"), settingsPanel); + tabbedPane.addTab("Server", UIManager.getIcon("Brainwine.serverIcon"), serverPanel = new ServerPanel(bootstrap)); + tabbedPane.addTab("Settings", UIManager.getIcon("Brainwine.settingsIcon"), settingsPanel = new SettingsPanel(this)); panel.add(tabbedPane); // Menu @@ -87,7 +86,7 @@ public class MainView { } }); frame.setJMenuBar(menuBar); - frame.setMinimumSize(new Dimension(848, 480)); + frame.setMinimumSize(new Dimension(848, 520)); frame.setFocusable(true); frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); frame.add(panel); @@ -96,6 +95,26 @@ public class MainView { frame.setVisible(true); } + public void setTabPlacement(int tabPlacement) { + setTabPlacement(tabPlacement, true); + } + + public void setTabPlacement(int tabPlacement, boolean animateChange) { + if(animateChange) { + FlatAnimatedLafChange.showSnapshot(); + } + + tabbedPane.setTabPlacement(Math.min(4, Math.max(1, tabPlacement))); + + if(animateChange) { + FlatAnimatedLafChange.hideSnapshotWithAnimation(); + } + } + + public int getTabPlacement() { + return tabbedPane.getTabPlacement(); + } + public void showHostSettings() { tabbedPane.setSelectedComponent(settingsPanel); settingsPanel.focusHostSettings(); diff --git a/src/main/java/brainwine/gui/SettingsPanel.java b/src/main/java/brainwine/gui/SettingsPanel.java index eb0eb20..32e8de2 100644 --- a/src/main/java/brainwine/gui/SettingsPanel.java +++ b/src/main/java/brainwine/gui/SettingsPanel.java @@ -13,7 +13,6 @@ import javax.swing.JComboBox; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; -import javax.swing.JSeparator; import javax.swing.JSpinner; import javax.swing.SpinnerNumberModel; import javax.swing.SwingUtilities; @@ -34,13 +33,17 @@ import brainwine.util.SwingUtils; @SuppressWarnings("serial") public class SettingsPanel extends JPanel { + private final MainView mainView; private JComboBox themeBox; + private JComboBox tabPlacementBox; private JSpinner fontSizeSpinner; - private JCheckBox embedMenuBarCheckox; + private JCheckBox embedMenuBarCheckbox; private FlatTextField gatewayHostField; private FlatTextField apiHostField; - public SettingsPanel() { + public SettingsPanel(MainView mainView) { + this.mainView = mainView; + // Reset Button JButton resetButton = new JButton("Reset to Defaults"); resetButton.addActionListener(event -> resetSettings(true)); @@ -51,6 +54,7 @@ public class SettingsPanel extends JPanel { // Button Panel JPanel buttonPanel = new JPanel(new GridLayout(1, 2)); + buttonPanel.setBorder(createCategoryBorder("Reset Settings")); buttonPanel.add(resetButton); buttonPanel.add(clearButton); @@ -62,8 +66,7 @@ public class SettingsPanel extends JPanel { settingsPanel.add(createGameSettingsPanel(), SwingUtils.createConstraints(0, 1)); } - settingsPanel.add(new JSeparator(), SwingUtils.createConstraints(0, 2)); - settingsPanel.add(buttonPanel, SwingUtils.createConstraints(0, 3)); + settingsPanel.add(buttonPanel, SwingUtils.createConstraints(0, 2)); // Scroll pane (TODO doesn't actually scroll) FlatScrollPane scrollPane = new FlatScrollPane(); @@ -79,26 +82,33 @@ public class SettingsPanel extends JPanel { themeBox = new JComboBox<>(); ThemeManager.getThemes().forEach(themeBox::addItem); themeBox.setSelectedItem(ThemeManager.getCurrentTheme()); - themeBox.addItemListener(item -> SwingUtilities.invokeLater(() -> ThemeManager.setTheme((Theme)item.getItem()))); + themeBox.addActionListener(event -> setThemePreference((Theme)themeBox.getSelectedItem())); + + // Tabbed pane orientation box + tabPlacementBox = new JComboBox<>(new String[] {"Top", "Left", "Bottom", "Right"}); + tabPlacementBox.setSelectedIndex(mainView.getTabPlacement() - 1); + tabPlacementBox.addActionListener(event -> setTabPlacementPreference(tabPlacementBox.getSelectedIndex() + 1)); // Font size changer fontSizeSpinner = new JSpinner(new SpinnerNumberModel(SwingUtils.getDefaultFontSize(), 10, 28, 1)); - fontSizeSpinner.addChangeListener(event -> SwingUtils.setDefaultFontSize((int)fontSizeSpinner.getValue())); - + fontSizeSpinner.addChangeListener(event -> setFontSizePreference((int)fontSizeSpinner.getValue())); + // Menu bar embed checkbox - embedMenuBarCheckox = new JCheckBox(); - embedMenuBarCheckox.setSelected(SwingUtils.isMenuBarEmbedded()); - embedMenuBarCheckox.addChangeListener(event -> SwingUtils.setMenuBarEmbedded(embedMenuBarCheckox.isSelected())); + embedMenuBarCheckbox = new JCheckBox(); + embedMenuBarCheckbox.setSelected(SwingUtils.isMenuBarEmbedded()); + embedMenuBarCheckbox.addChangeListener(event -> setEmbedMenuBarPreference(embedMenuBarCheckbox.isSelected())); // Panel JPanel panel = new JPanel(new GridBagLayout()); panel.setBorder(createCategoryBorder("Visual Settings")); panel.add(new JLabel("Theme"), SwingUtils.createConstraints(0, 0)); panel.add(themeBox, SwingUtils.createConstraints(1, 0)); - panel.add(new JLabel("Font Size"), SwingUtils.createConstraints(0, 1)); - panel.add(fontSizeSpinner, SwingUtils.createConstraints(1, 1)); - panel.add(new JLabel("Embed Menu Bar"), SwingUtils.createConstraints(0, 2)); - panel.add(embedMenuBarCheckox, SwingUtils.createConstraints(1, 2)); + panel.add(new JLabel("Tab Placement"), SwingUtils.createConstraints(0, 1)); + panel.add(tabPlacementBox, SwingUtils.createConstraints(1, 1)); + panel.add(new JLabel("Font Size"), SwingUtils.createConstraints(0, 2)); + panel.add(fontSizeSpinner, SwingUtils.createConstraints(1, 2)); + panel.add(new JLabel("Embed Menu Bar"), SwingUtils.createConstraints(0, 3)); + panel.add(embedMenuBarCheckbox, SwingUtils.createConstraints(1, 3)); return panel; } @@ -149,13 +159,40 @@ public class SettingsPanel extends JPanel { gatewayHostField.requestFocus(); } + private void setThemePreference(Theme theme) { + SwingUtilities.invokeLater(() -> { + ThemeManager.setTheme(theme); + }); + + GuiPreferences.setString(GuiPreferences.THEME_KEY, theme.getClassName()); + } + + private void setTabPlacementPreference(int tabPlacement) { + SwingUtilities.invokeLater(() -> { + mainView.setTabPlacement(tabPlacement); + }); + + GuiPreferences.setInt(GuiPreferences.TAB_PLACEMENT_KEY, tabPlacement); + } + + private void setFontSizePreference(int fontSize) { + SwingUtils.setDefaultFontSize(fontSize); + GuiPreferences.setInt(GuiPreferences.FONT_SIZE_KEY, fontSize); + } + + private void setEmbedMenuBarPreference(boolean embedMenuBar) { + SwingUtils.setMenuBarEmbedded(embedMenuBar); + GuiPreferences.setBoolean(GuiPreferences.EMBED_MENU_BAR_KEY, embedMenuBar); + } + private void resetSettings(boolean showPrompt) { if(!showPrompt || JOptionPane.showConfirmDialog(getRootPane(), "Are you sure you want to reset all settings to their default values?", "Confirmation", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) { themeBox.setSelectedItem(ThemeManager.getTheme(FlatMaterialDarkerIJTheme.class)); + tabPlacementBox.setSelectedIndex(0); fontSizeSpinner.setValue(14); - embedMenuBarCheckox.setSelected(true); + embedMenuBarCheckbox.setSelected(true); if(OperatingSystem.isWindows()) { gatewayHostField.setText("local"); diff --git a/src/main/java/brainwine/gui/component/ImagePanel.java b/src/main/java/brainwine/gui/component/ImagePanel.java new file mode 100644 index 0000000..6e9716f --- /dev/null +++ b/src/main/java/brainwine/gui/component/ImagePanel.java @@ -0,0 +1,72 @@ +package brainwine.gui.component; + +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.RenderingHints; + +import javax.swing.JPanel; + +/** + * A {@link JPanel} with a scaling and extending background image that always maintains its aspect ratio. + */ +@SuppressWarnings("serial") +public class ImagePanel extends JPanel { + + private Image image; + + public ImagePanel() { + super(); + } + + public ImagePanel(Image image) { + this.image = image; + } + + @Override + public void paintComponent(Graphics graphics) { + super.paintComponent(graphics); + + if(image == null) { + return; + } + + // Can this be simpler? + // Probably. Who knows. Don't really care. + int containerWidth = getWidth(); + int containerHeight = getHeight(); + int drawWidth = containerWidth; + int drawHeight = containerHeight; + float targetRatio = image.getWidth(this) / (float)image.getHeight(this); + float aspectRatio = drawWidth / (float)drawHeight; + float inverseTargetRatio = 1.0F / targetRatio; + + if(aspectRatio > targetRatio) { + drawWidth = Math.round(drawHeight * targetRatio); + } else if(aspectRatio < targetRatio) { + drawHeight = Math.round(drawWidth * inverseTargetRatio); + } + + if(drawWidth < containerWidth) { + drawHeight += (containerWidth - drawWidth) / targetRatio; + drawWidth = containerWidth; + } else if(drawHeight < containerHeight) { + drawWidth += (containerHeight - drawHeight) / inverseTargetRatio; + drawHeight = containerHeight; + } + + int x = (containerWidth - drawWidth) / 2; + int y = (containerHeight - drawHeight) / 2; + Graphics2D g2d = (Graphics2D)graphics; + g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g2d.drawImage(image, x, y, drawWidth, drawHeight, this); + } + + public void setImage(Image image) { + this.image = image; + } + + public Image getImage() { + return image; + } +} diff --git a/src/main/resources/background.jpg b/src/main/resources/background.jpg new file mode 100644 index 0000000..3853b47 Binary files /dev/null and b/src/main/resources/background.jpg differ