diff --git a/src/main/java/envoy/client/ui/ChatWindow.java b/src/main/java/envoy/client/ui/ChatWindow.java index 8e7dc55..182f824 100644 --- a/src/main/java/envoy/client/ui/ChatWindow.java +++ b/src/main/java/envoy/client/ui/ChatWindow.java @@ -65,7 +65,7 @@ public class ChatWindow extends JFrame { private PrimaryTextArea messageEnterTextArea = new PrimaryTextArea(space); private JList userList = new JList<>(); private DefaultListModel userListModel = new DefaultListModel<>(); - private ComponentList messageList = new ComponentList<>(new MessageListRenderer()); + private ComponentList messageList = new ComponentList<>(); private PrimaryScrollPane scrollPane = new PrimaryScrollPane(); private JTextPane textPane = new JTextPane(); private PrimaryButton postButton = new PrimaryButton("Post"); @@ -103,6 +103,7 @@ public class ChatWindow extends JFrame { public ChatWindow() { setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setBounds(100, 100, 600, 800); + setMinimumSize(new Dimension(400, 300)); setTitle("Envoy"); setLocationRelativeTo(null); setIconImage(Toolkit.getDefaultToolkit().createImage(getClass().getClassLoader().getResource("envoy_logo.png"))); @@ -129,9 +130,12 @@ public class ChatWindow extends JFrame { scrollPane.setViewportView(messageList); scrollPane.addComponentListener(new ComponentAdapter() { - // updates list elements when list is resized + // Update list elements when scroll pane (and thus list) is resized @Override - public void componentResized(ComponentEvent e) { messageList.synchronizeModel(); } + public void componentResized(ComponentEvent e) { + messageList.setMaximumSize(new Dimension(scrollPane.getWidth(), Integer.MAX_VALUE)); + messageList.synchronizeModel(); + } }); scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); @@ -608,6 +612,8 @@ public class ChatWindow extends JFrame { this.localDb = localDb; this.writeProxy = writeProxy; + messageList.setRenderer(new MessageListRenderer(client.getSender().getId())); + // Load users and chats new Thread(() -> { localDb.getUsers().values().forEach(user -> { diff --git a/src/main/java/envoy/client/ui/IconUtil.java b/src/main/java/envoy/client/ui/IconUtil.java new file mode 100644 index 0000000..25d6513 --- /dev/null +++ b/src/main/java/envoy/client/ui/IconUtil.java @@ -0,0 +1,61 @@ +package envoy.client.ui; + +import java.awt.Image; +import java.io.IOException; +import java.util.EnumMap; +import java.util.EnumSet; + +import javax.imageio.ImageIO; +import javax.swing.ImageIcon; + +/** + * Provides static utility methods for loading icons from the resource + * folder.
+ *
+ * Project: envoy-client + * File: IconUtil.java + * Created: 16.03.2020 + * + * @author Kai S. K. Engelbart + * @since Envoy v0.1-beta + */ +public class IconUtil { + + private IconUtil() {} + + /** + * Loads an icon from resource folder and scales it to a given size. + * + * @param path the path to the icon inside the resource folder + * @param size the size to scale the icon to + * @return the scaled icon + * @throws IOException if the loading process failed + * @since Envoy v0.1-beta + */ + public static ImageIcon load(String path, int size) throws IOException { + return new ImageIcon(ImageIO.read(IconUtil.class.getResourceAsStream(path)).getScaledInstance(size, size, Image.SCALE_SMOOTH)); + } + + /** + * + * Loads icons specified by an enum. The images have to be named like the + * lowercase enum constants with {@code .png} extension and be located inside a + * folder with the lowercase name of the enum, which must be contained inside + * the {@code /icons} folder. + * + * @param the enum that specifies the icons to load + * @param enumClass the class of the enum + * @param size the size to scale the icons to + * @return a map containing the loaded icons with the corresponding enum + * constants as keys + * @throws IOException if the loading process failed + * @since Envoy v0.1-beta + */ + public static > EnumMap loadByEnum(Class enumClass, int size) throws IOException { + var icons = new EnumMap(enumClass); + var path = "/icons/" + enumClass.getSimpleName().toLowerCase() + "/"; + for (var e : EnumSet.allOf(enumClass)) + icons.put(e, load(path + e.toString().toLowerCase() + ".png", size)); + return icons; + } +} diff --git a/src/main/java/envoy/client/ui/list/ComponentList.java b/src/main/java/envoy/client/ui/list/ComponentList.java index ed74b61..3d90ea9 100644 --- a/src/main/java/envoy/client/ui/list/ComponentList.java +++ b/src/main/java/envoy/client/ui/list/ComponentList.java @@ -31,6 +31,8 @@ public class ComponentList extends JPanel { private static final long serialVersionUID = 1759644503942876737L; + public ComponentList() { setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); } + /** * Creates an instance of {@link ComponentList}. * @@ -39,8 +41,8 @@ public class ComponentList extends JPanel { * @since Envoy v0.3-alpha */ public ComponentList(ComponentListCellRenderer renderer) { - setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); - this.renderer = renderer; + this(); + setRenderer(renderer); } /** @@ -53,7 +55,6 @@ public class ComponentList extends JPanel { */ public ComponentList(ComponentListModel model, ComponentListCellRenderer renderer) { this(renderer); - this.model = model; setModel(model); } @@ -200,6 +201,18 @@ public class ComponentList extends JPanel { */ public ComponentListModel getModel() { return model; } + /** + * @return the renderer + * @since Envoy v0.1-beta + */ + public ComponentListCellRenderer getRenderer() { return renderer; } + + /** + * @param renderer the renderer to set + * @since Envoy v0.1-beta + */ + public void setRenderer(ComponentListCellRenderer renderer) { this.renderer = renderer; } + /** * @return the multipleSelectionEnabled * @since Envoy v0.1-beta diff --git a/src/main/java/envoy/client/ui/renderer/MessageListRenderer.java b/src/main/java/envoy/client/ui/renderer/MessageListRenderer.java index 047f12c..27a5a0d 100644 --- a/src/main/java/envoy/client/ui/renderer/MessageListRenderer.java +++ b/src/main/java/envoy/client/ui/renderer/MessageListRenderer.java @@ -1,17 +1,15 @@ package envoy.client.ui.renderer; -import java.awt.Font; -import java.awt.image.BufferedImage; +import java.awt.*; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.EnumMap; -import javax.imageio.ImageIO; import javax.swing.*; import envoy.client.data.Settings; import envoy.client.ui.Color; -import envoy.client.ui.Theme; +import envoy.client.ui.IconUtil; import envoy.client.ui.list.ComponentList; import envoy.client.ui.list.ComponentListCellRenderer; import envoy.data.Message; @@ -31,89 +29,111 @@ import envoy.data.Message.MessageStatus; */ public class MessageListRenderer implements ComponentListCellRenderer { - private static final EnumMap statusIcons = new EnumMap<>(MessageStatus.class); + private static EnumMap statusIcons; + private static ImageIcon forwardIcon; static { - for (MessageStatus ms : MessageStatus.values()) - try { - statusIcons.put(ms, ImageIO.read(MessageListRenderer.class.getResourceAsStream(ms.toString().toLowerCase() + "_icon.png"))); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private JTextArea messageTextArea; - - @Override - public JPanel getListCellComponent(ComponentList list, Message value, boolean isSelected) { - final JPanel panel = new JPanel(); - panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); - final Theme theme = Settings.getInstance().getThemes().get(Settings.getInstance().getCurrentTheme()); - - // Panel background - panel.setBackground(isSelected ? theme.getSelectionColor() : theme.getCellColor()); - - // TODO: Handle message attachments - - final String state = value.getStatus().toString(); - final String date = new SimpleDateFormat("dd.MM.yyyy HH:mm").format(value.getCreationDate()); - final String text = value.getText(); - - // The Label that displays the creation date of a message - JLabel dateLabel = new JLabel(date); - // Set the date color to be the value of DateColorChat - dateLabel.setForeground(theme.getDateColor()); - - panel.add(dateLabel); - - if (value.isForwarded()) try { - var forwardLabel = new JLabel("Forwarded", new ImageIcon(ClassLoader.getSystemResourceAsStream(null).readAllBytes()), - SwingConstants.CENTER); - forwardLabel.setBackground(panel.getBackground()); - forwardLabel.setForeground(Color.lightGray); - panel.add(forwardLabel); + try { + statusIcons = IconUtil.loadByEnum(MessageStatus.class, 16); + forwardIcon = IconUtil.load("/icons/forward.png", 16); } catch (IOException e) { e.printStackTrace(); } + } - // The JTextArea that displays the text content of a message and its status - messageTextArea = new JTextArea(text + System.getProperty("line.separator")); + private final long senderId; + + /** + * Initializes a message list renderer. Messages with the given sender ID will + * be aligned on the right side, while all other messages will be aligned on + * the left side + * + * @param senderId the sender ID of the messages to align on the right side + * @since Envoy v0.1-beta + */ + public MessageListRenderer(long senderId) { this.senderId = senderId; } + + // TODO: Handle message attachments + + @Override + public JPanel getListCellComponent(ComponentList list, Message message, boolean isSelected) { + final var theme = Settings.getInstance().getThemes().get(Settings.getInstance().getCurrentTheme()); + + // Panel + final var panel = new JPanel(); + final int padding = (int) (list.getWidth() * 0.35); + + GridBagLayout gbl_panel = new GridBagLayout(); + gbl_panel.columnWidths = new int[] { 1, 1 }; + gbl_panel.rowHeights = new int[] { 1, 1 }; + gbl_panel.columnWeights = new double[] { 1, 1 }; + gbl_panel.rowWeights = new double[] { 1, 1 }; + + panel.setLayout(gbl_panel); + panel.setBackground(isSelected ? theme.getSelectionColor() : theme.getCellColor()); + + // Date Label - The Label that displays the creation date of a message + var dateLabel = new JLabel(new SimpleDateFormat("dd.MM.yyyy HH:mm").format(message.getCreationDate())); + dateLabel.setForeground(theme.getDateColor()); + dateLabel.setAlignmentX(1f); + dateLabel.setFont(new Font("Arial", Font.PLAIN, 12)); + dateLabel.setPreferredSize(dateLabel.getPreferredSize()); + + var gbc_dateLabel = new GridBagConstraints(); + gbc_dateLabel.fill = GridBagConstraints.BOTH; + gbc_dateLabel.gridx = 0; + gbc_dateLabel.gridy = 0; + panel.add(dateLabel, gbc_dateLabel); + + // Message area - The JTextArea that displays the text content of a message. + var messageTextArea = new JTextArea(message.getText()); messageTextArea.setLineWrap(true); messageTextArea.setWrapStyleWord(true); - messageTextArea.setAlignmentX(0.5f); messageTextArea.setForeground(theme.getMessageTextColor()); - messageTextArea.setBackground(panel.getBackground()); + messageTextArea.setAlignmentX(0.5f); + messageTextArea.setBackground(theme.getCellColor()); messageTextArea.setEditable(false); + var font = new Font("Arial", Font.PLAIN, 14); + messageTextArea.setFont(font); + messageTextArea.setSize(list.getMaximumSize().width - padding - 16, 10); - panel.add(messageTextArea); + var gbc_messageTextArea = new GridBagConstraints(); + gbc_messageTextArea.fill = GridBagConstraints.HORIZONTAL; + gbc_messageTextArea.gridx = 0; + gbc_messageTextArea.gridy = 1; + panel.add(messageTextArea, gbc_messageTextArea); - JLabel statusLabel = new JLabel(state); - statusLabel.setFont(new Font("Arial", Font.BOLD, 14)); - Color statusColor; - switch (value.getStatus()) { - case WAITING: - statusColor = Color.gray; - break; - case SENT: - statusColor = Color.blue; - break; - case RECEIVED: - statusColor = Color.yellow; - break; - case READ: - statusColor = Color.green; - break; - default: - statusColor = theme.getMessageTextColor(); - break; + // Status Label - displays the status of the message + var statusLabel = new JLabel(statusIcons.get(message.getStatus())); + + var gbc_statusLabel = new GridBagConstraints(); + gbc_statusLabel.gridx = 1; + gbc_statusLabel.gridy = 1; + panel.add(statusLabel, gbc_statusLabel); + + // Forwarding + if (message.isForwarded()) { + var forwardLabel = new JLabel("Forwarded", forwardIcon, SwingConstants.CENTER); + forwardLabel.setBackground(panel.getBackground()); + forwardLabel.setForeground(Color.lightGray); + + var gbc_forwardLabel = new GridBagConstraints(); + gbc_forwardLabel.fill = GridBagConstraints.BOTH; + gbc_forwardLabel.gridx = 1; + gbc_forwardLabel.gridy = 0; + panel.add(forwardLabel, gbc_forwardLabel); } - statusLabel.setForeground(statusColor); - statusLabel.setBackground(panel.getBackground()); - panel.add(statusLabel); + // Define an etched border and some space to the messages below + var ours = senderId == message.getSenderId(); + panel.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEmptyBorder(0, ours ? padding : 0, 10, ours ? 0 : padding), + BorderFactory.createEtchedBorder())); - // Define some space to the messages below - panel.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEmptyBorder(), BorderFactory.createEtchedBorder())); + var size = new Dimension(list.getMaximumSize().width - 50, panel.getPreferredSize().height); + + panel.setPreferredSize(size); + panel.setMinimumSize(size); + panel.setMaximumSize(size); return panel; } diff --git a/src/main/resources/icons/forward.png b/src/main/resources/icons/forward.png new file mode 100644 index 0000000..9c75854 Binary files /dev/null and b/src/main/resources/icons/forward.png differ diff --git a/src/main/resources/icons/messagestatus/read.png b/src/main/resources/icons/messagestatus/read.png new file mode 100644 index 0000000..d81f23c Binary files /dev/null and b/src/main/resources/icons/messagestatus/read.png differ diff --git a/src/main/resources/icons/messagestatus/received.png b/src/main/resources/icons/messagestatus/received.png new file mode 100644 index 0000000..3da9282 Binary files /dev/null and b/src/main/resources/icons/messagestatus/received.png differ diff --git a/src/main/resources/icons/messagestatus/sent.png b/src/main/resources/icons/messagestatus/sent.png new file mode 100644 index 0000000..2ff4cca Binary files /dev/null and b/src/main/resources/icons/messagestatus/sent.png differ diff --git a/src/main/resources/icons/messagestatus/waiting.png b/src/main/resources/icons/messagestatus/waiting.png new file mode 100644 index 0000000..e767b28 Binary files /dev/null and b/src/main/resources/icons/messagestatus/waiting.png differ diff --git a/src/main/resources/icons/settings.png b/src/main/resources/icons/settings.png new file mode 100644 index 0000000..f956615 Binary files /dev/null and b/src/main/resources/icons/settings.png differ