From 10dd3635a5e510c88947b140b6c4dfb4fef589de Mon Sep 17 00:00:00 2001 From: kske Date: Sat, 21 Dec 2019 00:29:16 +0100 Subject: [PATCH] Refined theme customization mechanism * Created abstract SettingsPanel class for defining settings screen pages * Moves theme customization related settings to new class ThemeCustomizationPanel * Changes Theme to use a map internally --- src/main/java/envoy/client/ui/ChatWindow.java | 2 +- .../java/envoy/client/ui/SettingsScreen.java | 458 ------------------ src/main/java/envoy/client/ui/Theme.java | 109 ++--- .../client/ui/settings/SettingsPanel.java | 23 + .../client/ui/settings/SettingsScreen.java | 187 +++++++ .../ui/settings/ThemeCustomizationPanel.java | 252 ++++++++++ src/main/resources/envoy_logo_old.png | Bin 8152 -> 0 bytes 7 files changed, 496 insertions(+), 535 deletions(-) delete mode 100644 src/main/java/envoy/client/ui/SettingsScreen.java create mode 100644 src/main/java/envoy/client/ui/settings/SettingsPanel.java create mode 100644 src/main/java/envoy/client/ui/settings/SettingsScreen.java create mode 100644 src/main/java/envoy/client/ui/settings/ThemeCustomizationPanel.java delete mode 100644 src/main/resources/envoy_logo_old.png diff --git a/src/main/java/envoy/client/ui/ChatWindow.java b/src/main/java/envoy/client/ui/ChatWindow.java index ea8ab1e..8b1c60c 100644 --- a/src/main/java/envoy/client/ui/ChatWindow.java +++ b/src/main/java/envoy/client/ui/ChatWindow.java @@ -29,6 +29,7 @@ import envoy.client.LocalDB; import envoy.client.Settings; import envoy.client.event.EventBus; import envoy.client.event.ThemeChangeEvent; +import envoy.client.ui.settings.SettingsScreen; import envoy.client.util.EnvoyLog; import envoy.schema.Message; import envoy.schema.User; @@ -157,7 +158,6 @@ public class ChatWindow extends JFrame { settingsButton.addActionListener((evt) -> { try { new SettingsScreen().setVisible(true); - changeChatWindowColors(Settings.getInstance().getThemes().get(Settings.getInstance().getCurrentTheme())); } catch (Exception e) { logger.log(Level.WARNING, "An error occured while opening the settings screen", e); e.printStackTrace(); diff --git a/src/main/java/envoy/client/ui/SettingsScreen.java b/src/main/java/envoy/client/ui/SettingsScreen.java deleted file mode 100644 index 75ce884..0000000 --- a/src/main/java/envoy/client/ui/SettingsScreen.java +++ /dev/null @@ -1,458 +0,0 @@ -package envoy.client.ui; - -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.event.ItemEvent; -import java.awt.event.ItemListener; -import java.util.Arrays; -import java.util.logging.Level; -import java.util.logging.Logger; - -import javax.swing.BoxLayout; -import javax.swing.DefaultListModel; -import javax.swing.JButton; -import javax.swing.JColorChooser; -import javax.swing.JComboBox; -import javax.swing.JDialog; -import javax.swing.JList; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JTextPane; -import javax.swing.ListSelectionModel; - -import envoy.client.Settings; -import envoy.client.util.EnvoyLog; -import envoy.client.event.EventBus; -import envoy.client.event.ThemeChangeEvent; - -/** - * This class provides the GUI to change the user specific settings. - * - * Project: envoy-client
- * File: SettingsScreen.java
- * Created: 31 Oct 2019
- * - * @author Leon Hofmeister - * @author Maximilian Käfer - * @author Kai S. K. Engelbart - */ -public class SettingsScreen extends JDialog { - - private static final long serialVersionUID = -4476913491263077107L; - private final JPanel contentPanel = new JPanel(); - - private DefaultListModel optionsListModel = new DefaultListModel<>(); - private final JList options = new JList<>(); - private JPanel buttonPane = new JPanel(); - - private JPanel themeContent = new JPanel(); - private String[] themeArray = Settings.getInstance().getThemes().keySet().toArray(new String[0]); - private JComboBox themes = new JComboBox<>(themeArray); - - private GridBagConstraints gbc_themeContent = new GridBagConstraints(); - - private JButton createNewThemeButton = new JButton("Create New Theme"); - private JPanel colorsPanel = new JPanel(); - private JButton okButton = new JButton("Save"); - private JButton cancelButton = new JButton("Cancel"); - - private static int space = 5; - - private Theme temporaryTheme, selectedTheme; - - private static final Logger logger = EnvoyLog.getLogger(SettingsScreen.class.getSimpleName()); - - /** - * Builds the settings screen. - * - * @since Envoy v0.1-alpha - */ - public SettingsScreen() { - logger.info(Settings.getInstance().getCurrentTheme()); - - setBounds(10, 10, 450, 650); - getContentPane().setLayout(new BorderLayout()); - { - - createNewThemeButton.setEnabled(false); - - temporaryTheme = new Theme("temporaryTheme", Settings.getInstance().getThemes().get(Settings.getInstance().getCurrentTheme())); - - // Content pane - GridBagLayout gbl_contentPanel = new GridBagLayout(); - - gbl_contentPanel.columnWidths = new int[] { 1, 1 }; - gbl_contentPanel.rowHeights = new int[] { 1 }; - gbl_contentPanel.columnWeights = new double[] { 0.05, 1.0 }; - gbl_contentPanel.rowWeights = new double[] { 1.0 }; - - getContentPane().add(contentPanel, BorderLayout.CENTER); - contentPanel.setLayout(gbl_contentPanel); - - options.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - - options.addListSelectionListener((listSelectionEvent) -> { - if (!listSelectionEvent.getValueIsAdjusting()) { - @SuppressWarnings("unchecked") - final JList selectedOption = (JList) listSelectionEvent.getSource(); - final String option = selectedOption.getSelectedValue(); - logger.log(Level.FINEST, option); - - switch (option) { - case "Color Themes": - setContent(themeContent, gbc_themeContent); - getContentPane().repaint(); - getContentPane().revalidate(); - break; - } - } - }); - - options.setFont(new Font("Arial", Font.PLAIN, 14)); - - Theme theme = Settings.getInstance().getThemes().get(Settings.getInstance().getCurrentTheme()); - - GridBagConstraints gbc_optionsList = new GridBagConstraints(); - gbc_optionsList.fill = GridBagConstraints.BOTH; - gbc_optionsList.gridx = 0; - gbc_optionsList.gridy = 0; - gbc_optionsList.anchor = GridBagConstraints.PAGE_START; - gbc_optionsList.insets = new Insets(space, space, space, space); - - optionsListModel.addElement("Color Themes"); - options.setModel(optionsListModel); - contentPanel.add(options, gbc_optionsList); - - // Theme content - gbc_themeContent = new GridBagConstraints(); - gbc_themeContent.fill = GridBagConstraints.BOTH; - gbc_themeContent.gridx = 1; - gbc_themeContent.gridy = 0; - gbc_themeContent.anchor = GridBagConstraints.PAGE_START; - gbc_themeContent.insets = new Insets(space, space, space, space); - - GridBagLayout gbl_themeLayout = new GridBagLayout(); - - gbl_themeLayout.columnWidths = new int[] { 1, 1 }; - gbl_themeLayout.rowHeights = new int[] { 1, 1 }; - gbl_themeLayout.columnWeights = new double[] { 1.0, 1.0 }; - gbl_themeLayout.rowWeights = new double[] { 0.01, 1.0 }; - - themeContent.setLayout(gbl_themeLayout); - - themes.setSelectedItem(Settings.getInstance().getCurrentTheme()); - - themes.addItemListener(new ItemListener() { - - @Override - public void itemStateChanged(ItemEvent e) { - String selectedValue = (String) themes.getSelectedItem(); - logger.log(Level.FINEST, selectedValue); - selectedTheme = Settings.getInstance().getThemes().get(selectedValue); - } - }); - - GridBagConstraints gbc_themes = new GridBagConstraints(); - gbc_themes.fill = GridBagConstraints.HORIZONTAL; - gbc_themes.gridx = 0; - gbc_themes.gridy = 0; - gbc_themes.anchor = GridBagConstraints.NORTHWEST; - gbc_themes.insets = new Insets(space, space, space, space); - - themeContent.add(themes, gbc_themes); - - colorsPanel.setLayout(new BoxLayout(colorsPanel, BoxLayout.Y_AXIS)); - colorsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); - - buildCustomizeElement(new JPanel(), new JButton(), new JTextPane(), theme, theme.getBackgroundColor(), "Background", 0); - buildCustomizeElement(new JPanel(), new JButton(), new JTextPane(), theme, theme.getCellColor(), "Cells", 1); - buildCustomizeElement(new JPanel(), - new JButton(), - new JTextPane(), - theme, - theme.getInteractableForegroundColor(), - "Interactable Foreground", - 2); - buildCustomizeElement(new JPanel(), - new JButton(), - new JTextPane(), - theme, - theme.getInteractableBackgroundColor(), - "Interactable Background", - 3); - buildCustomizeElement(new JPanel(), new JButton(), new JTextPane(), theme, theme.getMessageColorChat(), "Messages Chat", 4); - buildCustomizeElement(new JPanel(), new JButton(), new JTextPane(), theme, theme.getDateColorChat(), "Date Chat", 5); - buildCustomizeElement(new JPanel(), new JButton(), new JTextPane(), theme, theme.getSelectionColor(), "Selection", 6); - buildCustomizeElement(new JPanel(), new JButton(), new JTextPane(), theme, theme.getTypingMessageColor(), "Typing Message", 7); - buildCustomizeElement(new JPanel(), new JButton(), new JTextPane(), theme, theme.getUserNameColor(), "User Names", 8); - - GridBagConstraints gbc_colorsPanel = new GridBagConstraints(); - gbc_colorsPanel.fill = GridBagConstraints.HORIZONTAL; - gbc_colorsPanel.gridx = 0; - gbc_colorsPanel.gridy = 1; - gbc_colorsPanel.gridwidth = 2; - gbc_colorsPanel.anchor = GridBagConstraints.NORTHWEST; - gbc_colorsPanel.insets = new Insets(space, 0, 0, 0); - - themeContent.add(colorsPanel, gbc_colorsPanel); - - createNewThemeButton.setBackground(theme.getInteractableBackgroundColor()); - createNewThemeButton.setForeground(theme.getInteractableForegroundColor()); - colorsPanel.setBackground(theme.getCellColor()); - - createNewThemeButton.addActionListener((evt) -> { - try { - String s = JOptionPane.showInputDialog("Enter a name for the new theme"); - logger.log(Level.FINEST, s); - Settings.getInstance() - .addNewThemeToMap(new Theme(s, temporaryTheme.getBackgroundColor(), temporaryTheme.getCellColor(), - temporaryTheme.getInteractableForegroundColor(), temporaryTheme.getInteractableBackgroundColor(), - temporaryTheme.getMessageColorChat(), temporaryTheme.getDateColorChat(), temporaryTheme.getSelectionColor(), - temporaryTheme.getTypingMessageColor(), temporaryTheme.getUserNameColor())); - themeArray = Arrays.copyOf(themeArray, themeArray.length + 1); - themeArray[themeArray.length - 1] = Settings.getInstance().getThemes().get(s).getThemeName(); - - temporaryTheme = new Theme("temporaryTheme", Settings.getInstance().getThemes().get(Settings.getInstance().getCurrentTheme())); - - createNewThemeButton.setEnabled(false); - themes.addItem(themeArray[themeArray.length - 1]); - - contentPanel.revalidate(); - contentPanel.repaint(); - - } catch (Exception e) { - logger.info("New theme couldn't be created! " + e); - e.printStackTrace(); - } - }); - - GridBagConstraints gbc_createNewTheme = new GridBagConstraints(); - gbc_createNewTheme.gridx = 0; - gbc_createNewTheme.gridy = 10; - - colorsPanel.add(createNewThemeButton, gbc_createNewTheme); - - // ButtonPane------------------------------------------------------- - GridBagLayout gbl_buttonPane = new GridBagLayout(); - gbl_buttonPane.columnWidths = new int[] { 100, 250, 100, 0 }; - gbl_buttonPane.rowHeights = new int[] { 25, 0 }; - gbl_buttonPane.columnWeights = new double[] { 0.0, 0.0, 0.0, Double.MIN_VALUE }; - gbl_buttonPane.rowWeights = new double[] { 0.0, Double.MIN_VALUE }; - - getContentPane().add(buttonPane, BorderLayout.SOUTH); - buttonPane.setLayout(gbl_buttonPane); - { - cancelButton.setActionCommand("Cancel"); - cancelButton.setBorderPainted(false); - GridBagConstraints gbc_cancelButton = new GridBagConstraints(); - gbc_cancelButton.anchor = GridBagConstraints.NORTHWEST; - gbc_cancelButton.insets = new Insets(space, space, space, space); - gbc_cancelButton.gridx = 0; - gbc_cancelButton.gridy = 0; - buttonPane.add(cancelButton, gbc_cancelButton); - - cancelButton.addActionListener((evt) -> { dispose(); }); - } - { - okButton.setActionCommand("OK"); - okButton.setBorderPainted(false); - GridBagConstraints gbc_okButton = new GridBagConstraints(); - gbc_okButton.anchor = GridBagConstraints.NORTHEAST; - gbc_okButton.fill = GridBagConstraints.EAST; - gbc_okButton.insets = new Insets(space, space, space, space); - gbc_okButton.gridx = 2; - gbc_okButton.gridy = 0; - buttonPane.add(okButton, gbc_okButton); - getRootPane().setDefaultButton(okButton); - okButton.addActionListener((evt) -> { - try { - Settings.getInstance().setEnterToSend(Settings.getInstance().isEnterToSend());// still temporary - - Settings.getInstance().setCurrentTheme(selectedTheme.getThemeName()); - logger.log(Level.FINER, selectedTheme.getThemeName()); - - final Theme currentTheme = Settings.getInstance().getThemes().get(Settings.getInstance().getCurrentTheme()); - changeSettingsScreenColors(currentTheme); - updateColorVariables(currentTheme); - EventBus.getInstance().dispatch(new ThemeChangeEvent(currentTheme)); - Settings.getInstance().save(); - - revalidate(); - repaint(); - } catch (Exception e) { - logger.warning("Something went wrong when changing the setting"); - JOptionPane.showMessageDialog(this, "Something went wrong when changing the setting"); - dispose(); - } - }); - } - } - - changeSettingsScreenColors(Settings.getInstance().getThemes().get(Settings.getInstance().getCurrentTheme())); - - setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); - setModal(true); - } - - private void changeSettingsScreenColors(Theme theme) { - // whole JDialog - setBackground(theme.getBackgroundColor()); - // contentPanel - contentPanel.setBackground(theme.getBackgroundColor()); - // buttonPane - buttonPane.setBackground(theme.getCellColor()); - // cancelButton - cancelButton.setBackground(theme.getInteractableBackgroundColor()); - cancelButton.setForeground(theme.getInteractableForegroundColor()); - // okButton - okButton.setBackground(theme.getInteractableBackgroundColor()); - okButton.setForeground(theme.getInteractableForegroundColor()); - // options - options.setSelectionForeground(theme.getUserNameColor()); - options.setSelectionBackground(theme.getSelectionColor()); - options.setForeground(theme.getUserNameColor()); - options.setBackground(theme.getCellColor()); - // themeContent - themeContent.setForeground(theme.getUserNameColor()); - themeContent.setBackground(theme.getCellColor()); - // themes - themes.setBackground(theme.getBackgroundColor()); - themes.setForeground(getInvertedColor(theme.getBackgroundColor())); - - createNewThemeButton.setBackground(theme.getInteractableBackgroundColor()); - createNewThemeButton.setForeground(theme.getInteractableForegroundColor()); - colorsPanel.setBackground(theme.getCellColor()); - - } - - private void updateColorVariables(Theme theme) { - temporaryTheme = new Theme("temporaryTheme", theme); - - colorsPanel.removeAll(); - - buildCustomizeElement(new JPanel(), - - new JButton(), - new JTextPane(), - theme, - theme.getBackgroundColor(), - "Background", - 0); - buildCustomizeElement(new JPanel(), - - new JButton(), - new JTextPane(), - theme, - theme.getCellColor(), - "Cells", - 1); - buildCustomizeElement(new JPanel(), - - new JButton(), - new JTextPane(), - theme, - theme.getInteractableForegroundColor(), - "Interactable Foreground", - 2); - buildCustomizeElement(new JPanel(), - - new JButton(), - new JTextPane(), - theme, - theme.getInteractableBackgroundColor(), - "Interactable Background", - 3); - buildCustomizeElement(new JPanel(), - - new JButton(), - new JTextPane(), - theme, - theme.getMessageColorChat(), - "Messages Chat", - 4); - buildCustomizeElement(new JPanel(), - - new JButton(), - new JTextPane(), - theme, - theme.getDateColorChat(), - "Date Chat", - 5); - buildCustomizeElement(new JPanel(), - - new JButton(), - new JTextPane(), - theme, - theme.getSelectionColor(), - "Selection", - 6); - buildCustomizeElement(new JPanel(), - - new JButton(), - new JTextPane(), - theme, - theme.getTypingMessageColor(), - "Typing Message", - 7); - buildCustomizeElement(new JPanel(), - - new JButton(), - new JTextPane(), - theme, - theme.getUserNameColor(), - "User Names", - 8); - - GridBagConstraints gbc_createNewTheme = new GridBagConstraints(); - gbc_createNewTheme.gridx = 0; - gbc_createNewTheme.gridy = 10; - - colorsPanel.add(createNewThemeButton, gbc_createNewTheme); - } - - private void setContent(JPanel content, GridBagConstraints layout) { contentPanel.add(content, layout); } - - private void buildCustomizeElement(JPanel panel, JButton button, JTextPane textPane, Theme theme, Color color, String name, int yIndex) { - textPane.setFont(new Font("Arial", Font.PLAIN, 14)); - textPane.setBackground(theme.getBackgroundColor()); - textPane.setForeground(getInvertedColor(theme.getBackgroundColor())); - textPane.setText(name); - textPane.setEditable(false); - - button.setBackground(color); - button.setPreferredSize(new Dimension(25, 25)); - - button.addActionListener((evt) -> { - try { - Color newColor = JColorChooser.showDialog(null, "Choose a color", color); - if (newColor.getRGB() != color.getRGB()) { - logger.log(Level.FINEST, "New Color: " + String.valueOf(color.getRGB())); - // TODO: When Theme changed in same settings screen, color variable doesnt - // update. - temporaryTheme.setColor(yIndex, newColor); - createNewThemeButton.setEnabled(true); - } - button.setBackground(newColor); - - } catch (Exception e) { - logger.info("An error occured while opening Color Chooser: " + e); - e.printStackTrace(); - } - }); - - panel.add(textPane); - panel.add(button); - panel.setBackground(theme.getCellColor()); - panel.setAlignmentX(Component.LEFT_ALIGNMENT); - - colorsPanel.add(panel); - } - - private Color getInvertedColor(Color color) { return new Color(255 - color.getRed(), 255 - color.getGreen(), 255 - color.getBlue()); } -} diff --git a/src/main/java/envoy/client/ui/Theme.java b/src/main/java/envoy/client/ui/Theme.java index 78eccc6..c498655 100644 --- a/src/main/java/envoy/client/ui/Theme.java +++ b/src/main/java/envoy/client/ui/Theme.java @@ -2,6 +2,8 @@ package envoy.client.ui; import java.awt.Color; import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; /** * Project: envoy-client
@@ -15,16 +17,8 @@ public class Theme implements Serializable { private static final long serialVersionUID = 141727847527060352L; - private String themeName; - private Color backgroundColor; - private Color cellColor; - private Color interactableBackgroundColor; - private Color userNameColor; - private Color interactableForegroundColor; - private Color messageColorChat; - private Color dateColorChat; - private Color selectionColor; - private Color typingMessageColor; + private String themeName; + private Map colors = new HashMap<>(); /** * Initializes a {@link Theme} with all colors relevant to the application GUI. @@ -47,15 +41,15 @@ public class Theme implements Serializable { this.themeName = themeName; - this.backgroundColor = backgroundColor; - this.cellColor = cellColor; - this.interactableForegroundColor = interactableForegroundColor; - this.interactableBackgroundColor = interactableBackgroundColor; - this.messageColorChat = messageColorChat; - this.dateColorChat = dateColorChat; - this.selectionColor = selectionColor; - this.typingMessageColor = typingMessageColor; - this.userNameColor = userNameColor; + colors.put("backgroundColor", backgroundColor); + colors.put("cellColor", cellColor); + colors.put("interactableForegroundColor", interactableForegroundColor); + colors.put("interactableBackgroundColor", interactableBackgroundColor); + colors.put("messageColorChat", messageColorChat); + colors.put("dateColorChat", dateColorChat); + colors.put("selectionColor", selectionColor); + colors.put("typingMessageColor", typingMessageColor); + colors.put("userNameColor", userNameColor); } /** @@ -66,11 +60,16 @@ public class Theme implements Serializable { * @param other the {@link Theme} to copy */ public Theme(String name, Theme other) { - this(name, other.backgroundColor, other.cellColor, other.interactableForegroundColor, - other.interactableBackgroundColor, other.messageColorChat, other.dateColorChat, other.selectionColor, - other.typingMessageColor, other.userNameColor); + themeName = name; + colors.putAll(other.colors); } + /** + * @return a {@code Map} of all colors defined for this theme + * with their names as keys + */ + public Map getColors() { return colors; } + /** * @return name of the theme * @since Envoy v0.2-alpha @@ -81,104 +80,62 @@ public class Theme implements Serializable { * @return interactableForegroundColor * @since Envoy v0.2-alpha */ - public Color getInteractableForegroundColor() { return interactableForegroundColor; } + public Color getInteractableForegroundColor() { return colors.get("interactableForegroundColor"); } /** * @return messageColorChat * @since Envoy v0.2-alpha */ - public Color getMessageColorChat() { return messageColorChat; } + public Color getMessageColorChat() { return colors.get("messageColorChat"); } /** * @return dateColorChat * @since Envoy v0.2-alpha */ - public Color getDateColorChat() { return dateColorChat; } + public Color getDateColorChat() { return colors.get("dateColorChat"); } /** * @return selectionColor * @since Envoy v0.2-alpha */ - public Color getSelectionColor() { return selectionColor; } + public Color getSelectionColor() { return colors.get("selectionColor"); } /** * @return typingMessageColor * @since Envoy v0.2-alpha */ - public Color getTypingMessageColor() { return typingMessageColor; } + public Color getTypingMessageColor() { return colors.get("typingMessageColor"); } /** * @return backgroundColor * @since Envoy v0.2-alpha */ - public Color getBackgroundColor() { return backgroundColor; } + public Color getBackgroundColor() { return colors.get("backgroundColor"); } /** * @return cellColor * @since Envoy v0.2-alpha */ - public Color getCellColor() { return cellColor; } + public Color getCellColor() { return colors.get("cellColor"); } /** * @return interactableBackgroundColor * @since Envoy v0.2-alpha */ - public Color getInteractableBackgroundColor() { return interactableBackgroundColor; } + public Color getInteractableBackgroundColor() { return colors.get("interactableBackgroundColor"); } /** * @return userNameColor * @since Envoy v0.2-alpha */ - public Color getUserNameColor() { return userNameColor; } + public Color getUserNameColor() { return colors.get("userNameColor"); } /** * Sets the a specific {@link Color} in this theme to a new {@link Color} * - * @param index - index of the color
- * 0 = backgroundColor
- * 1 = cellColor
- * 2 = interactableForegroundColor
- * 3 = interactableBackgroundColor
- * 4 = messageColorChat
- * 5 = dateColorChat
- * 6 = selectionColor
- * 7 = typingMessageColor
- * 8 = userNameColor
- *
- * - * @param newColor - new {@link Color} to be set + * @param colorName the name of the {@link Color} to set + * @param newColor the new {@link Color} to be set * @since Envoy 0.2-alpha */ - public void setColor(int index, Color newColor) { - switch (index) { - case 0: - backgroundColor = newColor; - break; - case 1: - cellColor = newColor; - break; - case 2: - interactableForegroundColor = newColor; - break; - case 3: - interactableBackgroundColor = newColor; - break; - case 4: - messageColorChat = newColor; - break; - case 5: - dateColorChat = newColor; - break; - case 6: - selectionColor = newColor; - break; - case 7: - typingMessageColor = newColor; - break; - case 8: - userNameColor = newColor; - break; - } - } - + public void setColor(String colorName, Color newColor) { colors.put(colorName, newColor); } } diff --git a/src/main/java/envoy/client/ui/settings/SettingsPanel.java b/src/main/java/envoy/client/ui/settings/SettingsPanel.java new file mode 100644 index 0000000..56e1957 --- /dev/null +++ b/src/main/java/envoy/client/ui/settings/SettingsPanel.java @@ -0,0 +1,23 @@ +package envoy.client.ui.settings; + +import java.awt.event.ActionListener; + +import javax.swing.JPanel; + +/** + * Project: envoy-client
+ * File: SettingsPanel.java
+ * Created: 20 Dec 2019
+ * + * @author Kai S. K. Engelbart + */ +public abstract class SettingsPanel extends JPanel { + + private static final long serialVersionUID = -3069212622468626050L; + + /** + * @return an {@link ActionListener} that should be invoked when the OK button + * is pressed in the {@link SettingsScreen} + */ + public abstract ActionListener getOkButtonAction(); +} diff --git a/src/main/java/envoy/client/ui/settings/SettingsScreen.java b/src/main/java/envoy/client/ui/settings/SettingsScreen.java new file mode 100644 index 0000000..d34eee4 --- /dev/null +++ b/src/main/java/envoy/client/ui/settings/SettingsScreen.java @@ -0,0 +1,187 @@ +package envoy.client.ui.settings; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.swing.DefaultListModel; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.ListSelectionModel; + +import envoy.client.Settings; +import envoy.client.event.EventBus; +import envoy.client.event.ThemeChangeEvent; +import envoy.client.ui.Theme; +import envoy.client.util.EnvoyLog; + +/** + * This class provides the GUI to change the user specific settings.
+ *
+ * Project: envoy-client
+ * File: SettingsScreen.java
+ * Created: 31 Oct 2019
+ * + * @author Leon Hofmeister + * @author Maximilian Käfer + * @author Kai S. K. Engelbart + */ +public class SettingsScreen extends JDialog { + + private static final long serialVersionUID = -4476913491263077107L; + + private final JPanel contentPanel = new JPanel(); + + // Settings panel list + private final DefaultListModel optionsListModel = new DefaultListModel<>(); + private final JList options = new JList<>(optionsListModel); + + // OK and cancel buttons + private final JPanel buttonPane = new JPanel(); + private final JButton okButton = new JButton("Save"); + private final JButton cancelButton = new JButton("Cancel"); + + private final int space = 5; + + private SettingsPanel settingsPanel; + + private static final Logger logger = EnvoyLog.getLogger(SettingsScreen.class.getSimpleName()); + + /** + * Initializes the settings screen. + * + * @since Envoy v0.1-alpha + */ + public SettingsScreen() { + logger.info("Currently selected theme: " + Settings.getInstance().getCurrentTheme()); + + Map> panels = new HashMap<>(); + panels.put("Color Themes", ThemeCustomizationPanel.class); + + setBounds(10, 10, 450, 650); + getContentPane().setLayout(new BorderLayout()); + { + // Content pane + GridBagLayout gbl_contentPanel = new GridBagLayout(); + + gbl_contentPanel.columnWidths = new int[] { 1, 1 }; + gbl_contentPanel.rowHeights = new int[] { 1 }; + gbl_contentPanel.columnWeights = new double[] { 0.05, 1.0 }; + gbl_contentPanel.rowWeights = new double[] { 1.0 }; + + getContentPane().add(contentPanel, BorderLayout.CENTER); + contentPanel.setLayout(gbl_contentPanel); + + options.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + options.addListSelectionListener((listSelectionEvent) -> { + if (!listSelectionEvent.getValueIsAdjusting()) { + // Get selected settings panel + final String option = options.getSelectedValue(); + logger.log(Level.FINEST, "Selected settings panel: " + option); + + // Remove previous settings panel + if (settingsPanel != null) + contentPanel.remove(settingsPanel); + + try { + settingsPanel = panels.get(option).getDeclaredConstructor().newInstance(); + + // Add selected settings panel + contentPanel.add(settingsPanel); + revalidate(); + repaint(); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException + | NoSuchMethodException | SecurityException e) { + e.printStackTrace(); + } + } + }); + options.setFont(new Font("Arial", Font.PLAIN, 14)); + + GridBagConstraints gbc_optionsList = new GridBagConstraints(); + gbc_optionsList.fill = GridBagConstraints.BOTH; + gbc_optionsList.gridx = 0; + gbc_optionsList.gridy = 0; + gbc_optionsList.anchor = GridBagConstraints.PAGE_START; + gbc_optionsList.insets = new Insets(space, space, space, space); + + panels.keySet().forEach(name -> optionsListModel.addElement(name)); + contentPanel.add(options, gbc_optionsList); + + // ButtonPane + GridBagLayout gbl_buttonPane = new GridBagLayout(); + gbl_buttonPane.columnWidths = new int[] { 100, 250, 100, 0 }; + gbl_buttonPane.rowHeights = new int[] { 25, 0 }; + gbl_buttonPane.columnWeights = new double[] { 0.0, 0.0, 0.0, Double.MIN_VALUE }; + gbl_buttonPane.rowWeights = new double[] { 0.0, Double.MIN_VALUE }; + + getContentPane().add(buttonPane, BorderLayout.SOUTH); + buttonPane.setLayout(gbl_buttonPane); + { + cancelButton.setActionCommand("Cancel"); + cancelButton.setBorderPainted(false); + GridBagConstraints gbc_cancelButton = new GridBagConstraints(); + gbc_cancelButton.anchor = GridBagConstraints.NORTHWEST; + gbc_cancelButton.insets = new Insets(space, space, space, space); + gbc_cancelButton.gridx = 0; + gbc_cancelButton.gridy = 0; + buttonPane.add(cancelButton, gbc_cancelButton); + + cancelButton.addActionListener((evt) -> { dispose(); }); + } + { + okButton.setActionCommand("OK"); + okButton.setBorderPainted(false); + GridBagConstraints gbc_okButton = new GridBagConstraints(); + gbc_okButton.anchor = GridBagConstraints.NORTHEAST; + gbc_okButton.fill = GridBagConstraints.EAST; + gbc_okButton.insets = new Insets(space, space, space, space); + gbc_okButton.gridx = 2; + gbc_okButton.gridy = 0; + buttonPane.add(okButton, gbc_okButton); + getRootPane().setDefaultButton(okButton); + + // Invoke settings panel action on button press + okButton.addActionListener((evt) -> { if (settingsPanel != null) settingsPanel.getOkButtonAction().actionPerformed(evt); }); + } + } + + // Apply current theme + applyTheme(Settings.getInstance().getThemes().get(Settings.getInstance().getCurrentTheme())); + + // Respond to theme changes + EventBus.getInstance().register(ThemeChangeEvent.class, (evt) -> applyTheme(((ThemeChangeEvent) evt).get())); + + setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + setModal(true); + } + + private void applyTheme(Theme theme) { + // whole JDialog + setBackground(theme.getBackgroundColor()); + // contentPanel + contentPanel.setBackground(theme.getBackgroundColor()); + // buttonPane + buttonPane.setBackground(theme.getCellColor()); + // cancelButton + cancelButton.setBackground(theme.getInteractableBackgroundColor()); + cancelButton.setForeground(theme.getInteractableForegroundColor()); + // okButton + okButton.setBackground(theme.getInteractableBackgroundColor()); + okButton.setForeground(theme.getInteractableForegroundColor()); + // options + options.setSelectionForeground(theme.getUserNameColor()); + options.setSelectionBackground(theme.getSelectionColor()); + options.setForeground(theme.getUserNameColor()); + options.setBackground(theme.getCellColor()); + } +} diff --git a/src/main/java/envoy/client/ui/settings/ThemeCustomizationPanel.java b/src/main/java/envoy/client/ui/settings/ThemeCustomizationPanel.java new file mode 100644 index 0000000..61e484b --- /dev/null +++ b/src/main/java/envoy/client/ui/settings/ThemeCustomizationPanel.java @@ -0,0 +1,252 @@ +package envoy.client.ui.settings; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JColorChooser; +import javax.swing.JComboBox; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextPane; + +import envoy.client.Settings; +import envoy.client.event.EventBus; +import envoy.client.event.ThemeChangeEvent; +import envoy.client.ui.Theme; +import envoy.client.util.EnvoyLog; + +/** + * Project: envoy-client
+ * File: ThemeCustomizationPanel.java
+ * Created: 20 Dec 2019
+ * + * @author Kai S. K. Engelbart + */ +public class ThemeCustomizationPanel extends SettingsPanel { + + private static final long serialVersionUID = -8697897390666456624L; + + private JPanel colorsPanel = new JPanel(); + private JButton createNewThemeButton = new JButton("Create New Theme"); + + private String[] themeArray = Settings.getInstance().getThemes().keySet().toArray(new String[0]); + private JComboBox themes = new JComboBox<>(themeArray); + private Theme temporaryTheme, selectedTheme; + + private final int space = 5; + + private static final Logger logger = EnvoyLog.getLogger(ThemeCustomizationPanel.class.getSimpleName()); + + /** + * Initializes a {@link ThemeCustomizationPanel} that enables the user to change + * the current {@link Theme} and create new themes as part of the + * {@link SettingsScreen}. + */ + public ThemeCustomizationPanel() { + temporaryTheme = new Theme("temporaryTheme", Settings.getInstance().getThemes().get(Settings.getInstance().getCurrentTheme())); + + // Theme content + GridBagConstraints gbc_themeContent = new GridBagConstraints(); + gbc_themeContent.fill = GridBagConstraints.BOTH; + gbc_themeContent.gridx = 1; + gbc_themeContent.gridy = 0; + gbc_themeContent.anchor = GridBagConstraints.PAGE_START; + gbc_themeContent.insets = new Insets(space, space, space, space); + + GridBagLayout gbl_themeLayout = new GridBagLayout(); + + gbl_themeLayout.columnWidths = new int[] { 1, 1 }; + gbl_themeLayout.rowHeights = new int[] { 1, 1 }; + gbl_themeLayout.columnWeights = new double[] { 1.0, 1.0 }; + gbl_themeLayout.rowWeights = new double[] { 0.01, 1.0 }; + + setLayout(gbl_themeLayout); + + themes.setSelectedItem(Settings.getInstance().getCurrentTheme()); + + themes.addItemListener(new ItemListener() { + + @Override + public void itemStateChanged(ItemEvent e) { + String selectedValue = (String) themes.getSelectedItem(); + logger.log(Level.FINEST, selectedValue); + selectedTheme = Settings.getInstance().getThemes().get(selectedValue); + } + }); + + GridBagConstraints gbc_themes = new GridBagConstraints(); + gbc_themes.fill = GridBagConstraints.HORIZONTAL; + gbc_themes.gridx = 0; + gbc_themes.gridy = 0; + gbc_themes.anchor = GridBagConstraints.NORTHWEST; + gbc_themes.insets = new Insets(space, space, space, space); + + add(themes, gbc_themes); + + colorsPanel.setLayout(new BoxLayout(colorsPanel, BoxLayout.Y_AXIS)); + colorsPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + + Theme theme = Settings.getInstance().getThemes().get(Settings.getInstance().getCurrentTheme()); + buildCustomizeElements(theme); + + GridBagConstraints gbc_colorsPanel = new GridBagConstraints(); + gbc_colorsPanel.fill = GridBagConstraints.HORIZONTAL; + gbc_colorsPanel.gridx = 0; + gbc_colorsPanel.gridy = 1; + gbc_colorsPanel.gridwidth = 2; + gbc_colorsPanel.anchor = GridBagConstraints.NORTHWEST; + gbc_colorsPanel.insets = new Insets(space, 0, 0, 0); + + add(colorsPanel, gbc_colorsPanel); + + createNewThemeButton.setEnabled(false); + createNewThemeButton.setBackground(theme.getInteractableBackgroundColor()); + createNewThemeButton.setForeground(theme.getInteractableForegroundColor()); + colorsPanel.setBackground(theme.getCellColor()); + + createNewThemeButton.addActionListener((evt) -> { + try { + String s = JOptionPane.showInputDialog("Enter a name for the new theme"); + logger.log(Level.FINEST, s); + Settings.getInstance() + .addNewThemeToMap(new Theme(s, temporaryTheme.getBackgroundColor(), temporaryTheme.getCellColor(), + temporaryTheme.getInteractableForegroundColor(), temporaryTheme.getInteractableBackgroundColor(), + temporaryTheme.getMessageColorChat(), temporaryTheme.getDateColorChat(), temporaryTheme.getSelectionColor(), + temporaryTheme.getTypingMessageColor(), temporaryTheme.getUserNameColor())); + themeArray = Arrays.copyOf(themeArray, themeArray.length + 1); + themeArray[themeArray.length - 1] = Settings.getInstance().getThemes().get(s).getThemeName(); + + temporaryTheme = new Theme("temporaryTheme", Settings.getInstance().getThemes().get(Settings.getInstance().getCurrentTheme())); + + createNewThemeButton.setEnabled(false); + themes.addItem(themeArray[themeArray.length - 1]); + + } catch (Exception e) { + logger.info("New theme couldn't be created! " + e); + e.printStackTrace(); + } + }); + + GridBagConstraints gbc_createNewTheme = new GridBagConstraints(); + gbc_createNewTheme.gridx = 0; + gbc_createNewTheme.gridy = 10; + + colorsPanel.add(createNewThemeButton, gbc_createNewTheme); + + // Apply current theme + applyTheme(theme); + + // Respond to theme changes + EventBus.getInstance().register(ThemeChangeEvent.class, (evt) -> applyTheme(((ThemeChangeEvent) evt).get())); + } + + @Override + public ActionListener getOkButtonAction() { + return (evt) -> { + Settings.getInstance().setCurrentTheme(selectedTheme.getThemeName()); + logger.log(Level.FINER, "Setting theme: " + selectedTheme.getThemeName()); + + final Theme currentTheme = Settings.getInstance().getThemes().get(Settings.getInstance().getCurrentTheme()); + updateColorVariables(currentTheme); + EventBus.getInstance().dispatch(new ThemeChangeEvent(currentTheme)); + + revalidate(); + repaint(); + }; + } + + private void applyTheme(Theme theme) { + // themeContent + setForeground(theme.getUserNameColor()); + setBackground(theme.getCellColor()); + + // themes + themes.setBackground(theme.getBackgroundColor()); + themes.setForeground(getInvertedColor(theme.getBackgroundColor())); + + createNewThemeButton.setBackground(theme.getInteractableBackgroundColor()); + createNewThemeButton.setForeground(theme.getInteractableForegroundColor()); + colorsPanel.setBackground(theme.getCellColor()); + } + + private void updateColorVariables(Theme theme) { + temporaryTheme = new Theme("temporaryTheme", theme); + + colorsPanel.removeAll(); + + buildCustomizeElements(theme); + + GridBagConstraints gbc_createNewTheme = new GridBagConstraints(); + gbc_createNewTheme.gridx = 0; + gbc_createNewTheme.gridy = 10; + + colorsPanel.add(createNewThemeButton, gbc_createNewTheme); + } + + private void buildCustomizeElements(Theme theme) { + buildCustomizeElement(theme, theme.getBackgroundColor(), "Background", "backgroundColor"); + buildCustomizeElement(theme, theme.getCellColor(), "Cells", "cellColor"); + buildCustomizeElement(theme, theme.getInteractableForegroundColor(), "Interactable Foreground", "interactableForegroundColor"); + buildCustomizeElement(theme, theme.getInteractableBackgroundColor(), "Interactable Background", "interactableBackgroundColor"); + buildCustomizeElement(theme, theme.getMessageColorChat(), "Messages Chat", "messageColorChat"); + buildCustomizeElement(theme, theme.getDateColorChat(), "Date Chat", "dateColorCat"); + buildCustomizeElement(theme, theme.getSelectionColor(), "Selection", "selectionColor"); + buildCustomizeElement(theme, theme.getTypingMessageColor(), "Typing Message", "typingMessageColor"); + buildCustomizeElement(theme, theme.getUserNameColor(), "User Names", "userNameColor"); + } + + private void buildCustomizeElement(Theme theme, Color color, String name, String colorName) { + JPanel panel = new JPanel(); + JButton button = new JButton(); + JTextPane textPane = new JTextPane(); + + textPane.setFont(new Font("Arial", Font.PLAIN, 14)); + textPane.setBackground(theme.getBackgroundColor()); + textPane.setForeground(getInvertedColor(theme.getBackgroundColor())); + textPane.setText(name); + textPane.setEditable(false); + + button.setBackground(color); + button.setPreferredSize(new Dimension(25, 25)); + + button.addActionListener((evt) -> { + try { + Color newColor = JColorChooser.showDialog(null, "Choose a color", color); + if (newColor.getRGB() != color.getRGB()) { + logger.log(Level.FINEST, "New Color: " + String.valueOf(color.getRGB())); + // TODO: When Theme changed in same settings screen, color variable doesn't + // update + temporaryTheme.setColor(colorName, newColor); + createNewThemeButton.setEnabled(true); + } + button.setBackground(newColor); + + } catch (Exception e) { + logger.info("An error occured while opening Color Chooser: " + e); + e.printStackTrace(); + } + }); + + panel.add(textPane); + panel.add(button); + panel.setBackground(theme.getCellColor()); + panel.setAlignmentX(Component.LEFT_ALIGNMENT); + + colorsPanel.add(panel); + } + + private Color getInvertedColor(Color color) { return new Color(255 - color.getRed(), 255 - color.getGreen(), 255 - color.getBlue()); } +} diff --git a/src/main/resources/envoy_logo_old.png b/src/main/resources/envoy_logo_old.png deleted file mode 100644 index 35ef7d9fd78cc3b79454de104ae59fbb65c0b050..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8152 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