package envoy.client.ui; import java.awt.ComponentOrientation; import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Toolkit; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.DefaultListModel; import javax.swing.JFrame; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTextPane; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.border.EmptyBorder; import envoy.client.Chat; import envoy.client.Client; import envoy.client.Config; 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; /** * Project: envoy-client
* File: ChatWindow.java
* Created: 28 Sep 2019
* * @author Kai S. K. Engelbart * @author Maximilian Käfer * @author Leon Hofmeister * @since Envoy v0.1-alpha */ public class ChatWindow extends JFrame { private static final long serialVersionUID = 6865098428255463649L; // User specific objects private Client client; private LocalDB localDB; // GUI components private JPanel contentPane = new JPanel(); private PrimaryTextArea messageEnterTextArea = new PrimaryTextArea(space); private JList userList = new JList<>(); private Chat currentChat; private JList messageList = new JList<>(); private PrimaryScrollPane scrollPane = new PrimaryScrollPane(); private JTextPane textPane = new JTextPane(); private PrimaryButton postButton = new PrimaryButton("Post"); private PrimaryButton settingsButton = new PrimaryButton("Settings"); private static int space = 4; private static final Logger logger = EnvoyLog.getLogger(ChatWindow.class.getSimpleName()); /** * Initializes a {@link JFrame} with UI elements used to send and read messages * to different users. * * @param client the {@link Client} used to send and receive messages * @param localDB the {@link LocalDB} used to manage stored messages and users */ public ChatWindow(Client client, LocalDB localDB) { this.client = client; this.localDB = localDB; setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setBounds(100, 100, 600, 800); setTitle("Envoy"); setLocationRelativeTo(null); setIconImage(Toolkit.getDefaultToolkit().createImage(getClass().getClassLoader().getResource("envoy_logo.png"))); contentPane.setBorder(new EmptyBorder(space, space, space, space)); setContentPane(contentPane); GridBagLayout gbl_contentPane = new GridBagLayout(); gbl_contentPane.columnWidths = new int[] { 1, 1, 1 }; gbl_contentPane.rowHeights = new int[] { 1, 1, 1 }; gbl_contentPane.columnWeights = new double[] { 0.3, 1.0, 0.1 }; gbl_contentPane.rowWeights = new double[] { 0.05, 1.0, 0.07 }; contentPane.setLayout(gbl_contentPane); messageList.setCellRenderer(new MessageListRenderer()); messageList.setFocusTraversalKeysEnabled(false); messageList.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); DefaultListModel messageListModel = new DefaultListModel<>(); messageList.setModel(messageListModel); messageList.setFont(new Font("Arial", Font.PLAIN, 17)); messageList.setFixedCellHeight(60); messageList.setBorder(new EmptyBorder(space, space, space, space)); scrollPane.setViewportView(messageList); GridBagConstraints gbc_scrollPane = new GridBagConstraints(); gbc_scrollPane.fill = GridBagConstraints.BOTH; gbc_scrollPane.gridwidth = 2; gbc_scrollPane.gridx = 1; gbc_scrollPane.gridy = 1; gbc_scrollPane.insets = new Insets(space, space, space, space); contentPane.add(scrollPane, gbc_scrollPane); // Message enter field messageEnterTextArea.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ENTER && ((Settings.getInstance().isEnterToSend() && e.getModifiersEx() == 0) || (e.getModifiersEx() == KeyEvent.CTRL_DOWN_MASK))) postMessage(messageList); } }); GridBagConstraints gbc_messageEnterTextfield = new GridBagConstraints(); gbc_messageEnterTextfield.fill = GridBagConstraints.BOTH; gbc_messageEnterTextfield.gridx = 1; gbc_messageEnterTextfield.gridy = 2; gbc_messageEnterTextfield.insets = new Insets(space, space, space, space); contentPane.add(messageEnterTextArea, gbc_messageEnterTextfield); // Post Button GridBagConstraints gbc_moveSelectionPostButton = new GridBagConstraints(); gbc_moveSelectionPostButton.fill = GridBagConstraints.BOTH; gbc_moveSelectionPostButton.gridx = 2; gbc_moveSelectionPostButton.gridy = 2; gbc_moveSelectionPostButton.insets = new Insets(space, space, space, space); postButton.addActionListener((evt) -> { postMessage(messageList); }); contentPane.add(postButton, gbc_moveSelectionPostButton); // Settings Button GridBagConstraints gbc_moveSelectionSettingsButton = new GridBagConstraints(); gbc_moveSelectionSettingsButton.fill = GridBagConstraints.BOTH; gbc_moveSelectionSettingsButton.gridx = 2; gbc_moveSelectionSettingsButton.gridy = 0; gbc_moveSelectionSettingsButton.insets = new Insets(space, space, space, space); settingsButton.addActionListener((evt) -> { try { new SettingsScreen().setVisible(true); } catch (Exception e) { logger.log(Level.WARNING, "An error occured while opening the settings screen", e); e.printStackTrace(); } }); contentPane.add(settingsButton, gbc_moveSelectionSettingsButton); // Partner name display textPane.setFont(new Font("Arial", Font.PLAIN, 20)); textPane.setEditable(false); GridBagConstraints gbc_partnerName = new GridBagConstraints(); gbc_partnerName.fill = GridBagConstraints.HORIZONTAL; gbc_partnerName.gridx = 1; gbc_partnerName.gridy = 0; gbc_partnerName.insets = new Insets(space, space, space, space); contentPane.add(textPane, gbc_partnerName); userList.setCellRenderer(new UserListRenderer()); userList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); userList.addListSelectionListener((listSelectionEvent) -> { if (!listSelectionEvent.getValueIsAdjusting()) { @SuppressWarnings("unchecked") final JList selectedUserList = (JList) listSelectionEvent.getSource(); final User user = selectedUserList.getSelectedValue(); currentChat = localDB.getChats().stream().filter(chat -> chat.getRecipient().getID() == user.getID()).findFirst().get(); // Set all unread messages in the chat to read readCurrentChat(); client.setRecipient(user); textPane.setText(currentChat.getRecipient().getName()); messageList.setModel(currentChat.getModel()); scrollPane.setChatOpened(true); contentPane.revalidate(); } }); userList.setFont(new Font("Arial", Font.PLAIN, 17)); userList.setBorder(new EmptyBorder(space, space, space, space)); GridBagConstraints gbc_userList = new GridBagConstraints(); gbc_userList.fill = GridBagConstraints.VERTICAL; gbc_userList.gridx = 0; gbc_userList.gridy = 1; gbc_userList.anchor = GridBagConstraints.PAGE_START; gbc_userList.insets = new Insets(space, space, space, space); changeChatWindowColors(Settings.getInstance().getThemes().get(Settings.getInstance().getCurrentTheme())); contentPane.add(userList, gbc_userList); contentPane.revalidate(); EventBus.getInstance().register(ThemeChangeEvent.class, (evt) -> changeChatWindowColors((Theme) evt.get())); loadUsersAndChats(); if (client.isOnline()) startSyncThread(Config.getInstance().getSyncTimeout()); contentPane.revalidate(); } /** * Used to immediately reload the ChatWindow when settings were changed. * * @param theme the theme to change colors into * @since Envoy v0.1-alpha */ private void changeChatWindowColors(Theme theme) { // contentPane contentPane.setBackground(theme.getBackgroundColor()); contentPane.setForeground(theme.getUserNameColor()); // messageList messageList.setSelectionForeground(theme.getUserNameColor()); messageList.setSelectionBackground(theme.getSelectionColor()); messageList.setForeground(theme.getMessageColorChat()); messageList.setBackground(theme.getCellColor()); // scrollPane scrollPane.applyTheme(theme); scrollPane.autoscroll(); // messageEnterTextArea messageEnterTextArea.setCaretColor(theme.getTypingMessageColor()); messageEnterTextArea.setForeground(theme.getTypingMessageColor()); messageEnterTextArea.setBackground(theme.getCellColor()); // postButton postButton.setForeground(theme.getInteractableForegroundColor()); postButton.setBackground(theme.getInteractableBackgroundColor()); // settingsButton settingsButton.setForeground(theme.getInteractableForegroundColor()); settingsButton.setBackground(theme.getInteractableBackgroundColor()); // textPane textPane.setBackground(theme.getBackgroundColor()); textPane.setForeground(theme.getUserNameColor()); // userList userList.setSelectionForeground(theme.getUserNameColor()); userList.setSelectionBackground(theme.getSelectionColor()); userList.setForeground(theme.getUserNameColor()); userList.setBackground(theme.getCellColor()); } private void postMessage(JList messageList) { if (!client.hasRecipient()) { JOptionPane.showMessageDialog(this, "Please select a recipient!", "Cannot send message", JOptionPane.INFORMATION_MESSAGE); return; } if (!messageEnterTextArea.getText().isEmpty()) try { // Create and send message object final Message message = localDB.createMessage(messageEnterTextArea.getText(), currentChat.getRecipient().getID()); currentChat.appendMessage(message); messageList.setModel(currentChat.getModel()); // Clear text field messageEnterTextArea.setText(""); contentPane.revalidate(); } catch (Exception e) { JOptionPane.showMessageDialog(this, "An exception occured while sending a message. See the log for more details.", "Exception occured", JOptionPane.ERROR_MESSAGE); e.printStackTrace(); } } /** * Initializes the elements of the user list by downloading them from the * server. * * @since Envoy v0.1-alpha */ private void loadUsersAndChats() { new Thread(() -> { DefaultListModel userListModel = new DefaultListModel<>(); localDB.getUsers().values().forEach(user -> { userListModel.addElement(user); // Check if user exists in local DB if (localDB.getChats().stream().filter(c -> c.getRecipient().getID() == user.getID()).count() == 0) localDB.getChats().add(new Chat(user)); }); SwingUtilities.invokeLater(() -> userList.setModel(userListModel)); }).start(); } /** * Updates the data model and the UI repeatedly after a certain amount of * time. * * @param timeout the amount of time that passes between two requests sent to * the server * @since Envoy v0.1-alpha */ private void startSyncThread(int timeout) { new Timer(timeout, (evt) -> { new Thread(() -> { // Synchronize try { localDB.applySync(client.sendSync(client.getSender().getID(), localDB.fillSync(client.getSender().getID()))); } catch (Exception e) { logger.log(Level.SEVERE, "Could not perform sync", e); } // Process unread messages localDB.addUnreadMessagesToLocalDB(); localDB.clearUnreadMessagesSync(); // Mark unread messages as read when they are in the current chat readCurrentChat(); // Update UI SwingUtilities.invokeLater(() -> { updateUserStates(); contentPane.revalidate(); contentPane.repaint(); }); }).start(); }).start(); } private void updateUserStates() { for (int i = 0; i < userList.getModel().getSize(); i++) for (int j = 0; j < localDB.getChats().size(); j++) if (userList.getModel().getElementAt(i).getID() == localDB.getChats().get(j).getRecipient().getID()) userList.getModel().getElementAt(i).setStatus(localDB.getChats().get(j).getRecipient().getStatus()); } /** * Marks messages in the current chat as {@code READ}. */ private void readCurrentChat() { if (currentChat != null) { localDB.setMessagesToRead(currentChat); } } }