Merge pull request #93 from informatik-ag-ngl/f/component_list

Component List
This commit is contained in:
Kai S. K. Engelbart 2020-02-02 11:39:18 +01:00 committed by GitHub
commit bd61936fde
13 changed files with 437 additions and 153 deletions

View File

@ -28,11 +28,5 @@
<attribute name="maven.pomderived" value="true"/> <attribute name="maven.pomderived" value="true"/>
</attributes> </attributes>
</classpathentry> </classpathentry>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/> <classpathentry kind="output" path="target/classes"/>
</classpath> </classpath>

View File

@ -28,11 +28,16 @@
<dependency> <dependency>
<groupId>com.github.informatik-ag-ngl</groupId> <groupId>com.github.informatik-ag-ngl</groupId>
<artifactId>envoy-common</artifactId> <artifactId>envoy-common</artifactId>
<version>e5c67b8</version> <version>develop-SNAPSHOT</version>
</dependency> </dependency>
</dependencies> </dependencies>
<build> <build>
<finalName>envoy-client</finalName> <finalName>envoy-client</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build> </build>
</project> </project>

View File

@ -2,9 +2,9 @@ package envoy.client;
import java.io.Serializable; import java.io.Serializable;
import javax.swing.DefaultListModel; import envoy.client.ui.list.ComponentListModel;
import envoy.data.Message; import envoy.data.Message;
import envoy.data.Message.MessageStatus;
import envoy.data.User; import envoy.data.User;
/** /**
@ -25,7 +25,7 @@ public class Chat implements Serializable {
private static final long serialVersionUID = -7751248474547242056L; private static final long serialVersionUID = -7751248474547242056L;
private User recipient; private User recipient;
private DefaultListModel<Message> model = new DefaultListModel<>(); private ComponentListModel<Message> model = new ComponentListModel<>();
/** /**
* Provides the list of messages that the recipient receives.<br> * Provides the list of messages that the recipient receives.<br>
@ -37,22 +37,34 @@ public class Chat implements Serializable {
public Chat(User recipient) { this.recipient = recipient; } public Chat(User recipient) { this.recipient = recipient; }
/** /**
* @return the recipient of a message * Appends a message to the bottom of this chat
*
* @param message the message to append
* @since Envoy v0.1-alpha * @since Envoy v0.1-alpha
*/ */
public User getRecipient() { return recipient; } public void appendMessage(Message message) { model.add(message); }
/** /**
* Adds the received message at the current Point in the current chat * Sets the status of all chat messages to {@code READ} starting from the bottom
* and stopping once a read message is found.
* *
* @param message the message to add in said chat * @since Envoy v0.3-alpha
* @since Envoy v0.1-alpha
*/ */
public void appendMessage(Message message) { model.addElement(message); } public void read() {
for (int i = model.size() - 1; i >= 0; --i)
if (model.get(i).getStatus() == MessageStatus.READ) break;
else model.get(i).setStatus(MessageStatus.READ);
}
/** /**
* @return all messages in the current chat * @return all messages in the current chat
* @since Envoy v0.1-alpha * @since Envoy v0.1-alpha
*/ */
public DefaultListModel<Message> getModel() { return model; } public ComponentListModel<Message> getModel() { return model; }
/**
* @return the recipient of a message
* @since Envoy v0.1-alpha
*/
public User getRecipient() { return recipient; }
} }

View File

@ -11,6 +11,7 @@ import javax.naming.TimeLimitExceededException;
import envoy.client.util.EnvoyLog; import envoy.client.util.EnvoyLog;
import envoy.data.*; import envoy.data.*;
import envoy.event.IdGeneratorRequest;
import envoy.util.SerializationUtils; import envoy.util.SerializationUtils;
/** /**
@ -45,11 +46,13 @@ public class Client implements Closeable {
* an exception is thrown. * an exception is thrown.
* *
* @param credentials the login credentials of the user * @param credentials the login credentials of the user
* @param localDB the local database used to persist the current
* {@link IdGenerator}
* @throws Exception if the online mode could not be entered or the request * @throws Exception if the online mode could not be entered or the request
* failed for some other reason * failed for some other reason
* @since Envoy v0.2-alpha * @since Envoy v0.2-alpha
*/ */
public void onlineInit(LoginCredentials credentials) throws Exception { public void onlineInit(LoginCredentials credentials, LocalDB localDB) throws Exception {
// Establish TCP connection // Establish TCP connection
logger.info(String.format("Attempting connection to server %s:%d...", config.getServer(), config.getPort())); logger.info(String.format("Attempting connection to server %s:%d...", config.getServer(), config.getPort()));
socket = new Socket(config.getServer(), config.getPort()); socket = new Socket(config.getServer(), config.getPort());
@ -84,6 +87,13 @@ public class Client implements Closeable {
// Register processors for message and status handling // Register processors for message and status handling
receiver.registerProcessor(Message.class, new ReceivedMessageProcessor()); receiver.registerProcessor(Message.class, new ReceivedMessageProcessor());
// TODO: Status handling
// Process message ID generation
receiver.registerProcessor(IdGenerator.class, localDB::setIdGenerator);
// Request a generator if none is present
if (!localDB.hasIdGenerator() || !localDB.getIdGenerator().hasNext()) requestIdGenerator();
} }
/** /**
@ -94,11 +104,21 @@ public class Client implements Closeable {
* @since Envoy v0.3-alpha * @since Envoy v0.3-alpha
*/ */
public void sendMessage(Message message) throws IOException { public void sendMessage(Message message) throws IOException {
checkOnline(); writeObject(message);
SerializationUtils.writeBytesWithLength(message, socket.getOutputStream());
message.nextStatus(); message.nextStatus();
} }
/**
* Requests a new {@link IdGenerator} from the server.
*
* @throws IOException if the request does not reach the server
* @since Envoy v0.3-alpha
*/
public void requestIdGenerator() throws IOException {
logger.info("Requesting new id generator...");
writeObject(new IdGeneratorRequest());
}
/** /**
* @return a {@code Map<String, User>} of all users on the server with their * @return a {@code Map<String, User>} of all users on the server with their
* user names as keys * user names as keys
@ -114,6 +134,11 @@ public class Client implements Closeable {
@Override @Override
public void close() throws IOException { if (online) socket.close(); } public void close() throws IOException { if (online) socket.close(); }
private void writeObject(Object obj) throws IOException {
checkOnline();
SerializationUtils.writeBytesWithLength(obj, socket.getOutputStream());
}
private void checkOnline() { if (!online) throw new IllegalStateException("Client is not online"); } private void checkOnline() { if (!online) throw new IllegalStateException("Client is not online"); }
/** /**

View File

@ -31,7 +31,6 @@ public class Config {
items.put("server", new ConfigItem<>("server", "s", (input) -> input, null)); items.put("server", new ConfigItem<>("server", "s", (input) -> input, null));
items.put("port", new ConfigItem<>("port", "p", (input) -> Integer.parseInt(input), null)); items.put("port", new ConfigItem<>("port", "p", (input) -> Integer.parseInt(input), null));
items.put("localDB", new ConfigItem<>("localDB", "db", (input) -> new File(input), new File("localDB"))); items.put("localDB", new ConfigItem<>("localDB", "db", (input) -> new File(input), new File("localDB")));
items.put("syncTimeout", new ConfigItem<>("syncTimeout", "st", (input) -> Integer.parseInt(input), 1000));
items.put("homeDirectory", items.put("homeDirectory",
new ConfigItem<>("homeDirectory", "h", (input) -> new File(input), new File(System.getProperty("user.home"), ".envoy"))); new ConfigItem<>("homeDirectory", "h", (input) -> new File(input), new File(System.getProperty("user.home"), ".envoy")));
items.put("fileLevelBarrier", new ConfigItem<>("fileLevelBarrier", "fb", (input) -> Level.parse(input), Level.CONFIG)); items.put("fileLevelBarrier", new ConfigItem<>("fileLevelBarrier", "fb", (input) -> Level.parse(input), Level.CONFIG));
@ -112,7 +111,7 @@ public class Config {
* @return the port at which the Envoy server is located on the host * @return the port at which the Envoy server is located on the host
* @since Envoy v0.1-alpha * @since Envoy v0.1-alpha
*/ */
public int getPort() { return (int) items.get("port").get(); } public Integer getPort() { return (Integer) items.get("port").get(); }
/** /**
* @return the local database specific to the client user * @return the local database specific to the client user
@ -120,12 +119,6 @@ public class Config {
*/ */
public File getLocalDB() { return (File) items.get("localDB").get(); } public File getLocalDB() { return (File) items.get("localDB").get(); }
/**
* @return the current time (milliseconds) that is waited between Syncs
* @since Envoy v0.1-alpha
*/
public int getSyncTimeout() { return (int) items.get("syncTimeout").get(); }
/** /**
* @return the directory in which all local files are saves * @return the directory in which all local files are saves
* @since Envoy v0.2-alpha * @since Envoy v0.2-alpha

View File

@ -4,10 +4,15 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
import envoy.data.IdGenerator;
import envoy.data.User; import envoy.data.User;
import envoy.util.SerializationUtils; import envoy.util.SerializationUtils;
/** /**
* Stored information about the current {@link User} and their {@link Chat}s.
* For message ID generation a {@link IdGenerator} is stored as well.
* These object are persisted inside a folder of the local file system.<br>
* <br>
* Project: <strong>envoy-client</strong><br> * Project: <strong>envoy-client</strong><br>
* File: <strong>LocalDB.java</strong><br> * File: <strong>LocalDB.java</strong><br>
* Created: <strong>27.10.2019</strong><br> * Created: <strong>27.10.2019</strong><br>
@ -18,10 +23,11 @@ import envoy.util.SerializationUtils;
*/ */
public class LocalDB { public class LocalDB {
private File localDBDir, localDBFile, usersFile; private File localDBDir, localDBFile, usersFile, idGeneratorFile;
private User user; private User user;
private Map<String, User> users = new HashMap<>(); private Map<String, User> users = new HashMap<>();
private List<Chat> chats = new ArrayList<>(); private List<Chat> chats = new ArrayList<>();
private IdGenerator idGenerator;
/** /**
* Constructs an empty local database. To serialize any chats to the file * Constructs an empty local database. To serialize any chats to the file
@ -38,6 +44,7 @@ public class LocalDB {
if (localDBDir.exists() && !localDBDir.isDirectory()) if (localDBDir.exists() && !localDBDir.isDirectory())
throw new IOException(String.format("LocalDBDir '%s' is not a directory!", localDBDir.getAbsolutePath())); throw new IOException(String.format("LocalDBDir '%s' is not a directory!", localDBDir.getAbsolutePath()));
usersFile = new File(localDBDir, "users.db"); usersFile = new File(localDBDir, "users.db");
idGeneratorFile = new File(localDBDir, "id_generator.db");
} }
/** /**
@ -53,7 +60,8 @@ public class LocalDB {
/** /**
* Stores all users to the local database. If the client user is specified, the * Stores all users to the local database. If the client user is specified, the
* chats related to this user are stored as well. * chats related to this user are stored as well. The message id generator will
* also be saved if present.
* *
* @throws IOException if something went wrong during saving * @throws IOException if something went wrong during saving
* @since Envoy v0.1-alpha * @since Envoy v0.1-alpha
@ -63,7 +71,10 @@ public class LocalDB {
SerializationUtils.write(usersFile, users); SerializationUtils.write(usersFile, users);
// Save chats // Save chats
SerializationUtils.write(localDBFile, chats); if (user != null) SerializationUtils.write(localDBFile, chats);
// Save id generator
if (hasIdGenerator()) SerializationUtils.write(idGeneratorFile, idGenerator);
} }
/** /**
@ -84,6 +95,18 @@ public class LocalDB {
*/ */
public void loadChats() throws ClassNotFoundException, IOException { chats = SerializationUtils.read(localDBFile, ArrayList.class); } public void loadChats() throws ClassNotFoundException, IOException { chats = SerializationUtils.read(localDBFile, ArrayList.class); }
/**
* Loads the message ID generator that is stored in the local database. If the
* file is not found, the exception is ignored.
*
* @since Envoy v0.3-alpha
*/
public void loadIdGenerator() {
try {
idGenerator = SerializationUtils.read(idGeneratorFile, IdGenerator.class);
} catch (ClassNotFoundException | IOException e) {}
}
/** /**
* @return a {@code Map<String, User>} of all users stored locally with their * @return a {@code Map<String, User>} of all users stored locally with their
* user names as keys * user names as keys
@ -119,4 +142,22 @@ public class LocalDB {
* @since Envoy v0.2-alpha * @since Envoy v0.2-alpha
*/ */
public void setUser(User user) { this.user = user; } public void setUser(User user) { this.user = user; }
/**
* @return the message ID generator
* @since Envoy v0.3-alpha
*/
public IdGenerator getIdGenerator() { return idGenerator; }
/**
* @param idGenerator the message ID generator to set
* @since Envoy v0.3-alpha
*/
public void setIdGenerator(IdGenerator idGenerator) { this.idGenerator = idGenerator; }
/**
* @return {@code true} if an {@link IdGenerator} is present
* @since Envoy v0.3-alpha
*/
public boolean hasIdGenerator() { return idGenerator != null; }
} }

View File

@ -135,7 +135,7 @@ public class Settings {
* {@code Control} key. * {@code Control} key.
* @since Envoy v0.2-alpha * @since Envoy v0.2-alpha
*/ */
public boolean isEnterToSend() { return (boolean) items.get("enterToSend").get(); } public Boolean isEnterToSend() { return (Boolean) items.get("enterToSend").get(); }
/** /**
* Changes the keystrokes performed by the user to send a message. * Changes the keystrokes performed by the user to send a message.
@ -152,7 +152,7 @@ public class Settings {
* @return the current on close mode. * @return the current on close mode.
* @since Envoy v0.3-alpha * @since Envoy v0.3-alpha
*/ */
public boolean getCurrentOnCloseMode() { return (boolean) items.get("onCloseMode").get(); } public Boolean getCurrentOnCloseMode() { return (Boolean) items.get("onCloseMode").get(); }
/** /**
* Sets the current on close mode. * Sets the current on close mode.

View File

@ -10,7 +10,9 @@ import javax.swing.*;
import javax.swing.border.EmptyBorder; import javax.swing.border.EmptyBorder;
import envoy.client.*; import envoy.client.*;
import envoy.client.event.MessageCreationEvent;
import envoy.client.event.ThemeChangeEvent; import envoy.client.event.ThemeChangeEvent;
import envoy.client.ui.list.ComponentList;
import envoy.client.ui.settings.SettingsScreen; import envoy.client.ui.settings.SettingsScreen;
import envoy.client.util.EnvoyLog; import envoy.client.util.EnvoyLog;
import envoy.data.Message; import envoy.data.Message;
@ -30,8 +32,6 @@ import envoy.event.EventBus;
*/ */
public class ChatWindow extends JFrame { public class ChatWindow extends JFrame {
private static final long serialVersionUID = 6865098428255463649L;
// User specific objects // User specific objects
private Client client; private Client client;
private LocalDB localDB; private LocalDB localDB;
@ -41,16 +41,20 @@ public class ChatWindow extends JFrame {
private PrimaryTextArea messageEnterTextArea = new PrimaryTextArea(space); private PrimaryTextArea messageEnterTextArea = new PrimaryTextArea(space);
private JList<User> userList = new JList<>(); private JList<User> userList = new JList<>();
private Chat currentChat; private Chat currentChat;
private JList<Message> messageList = new JList<>(); private ComponentList<Message> messageList = new ComponentList<>(new MessageListRenderer());
private PrimaryScrollPane scrollPane = new PrimaryScrollPane(); private PrimaryScrollPane scrollPane = new PrimaryScrollPane();
private JTextPane textPane = new JTextPane(); private JTextPane textPane = new JTextPane();
private PrimaryButton postButton = new PrimaryButton("Post"); private PrimaryButton postButton = new PrimaryButton("Post");
private PrimaryButton settingsButton = new PrimaryButton("Settings"); private PrimaryButton settingsButton = new PrimaryButton("Settings");
private static int space = 4;
private static final Logger logger = EnvoyLog.getLogger(ChatWindow.class.getSimpleName()); private static final Logger logger = EnvoyLog.getLogger(ChatWindow.class.getSimpleName());
// GUI component spacing
private final static int space = 4;
private static final Insets insets = new Insets(space, space, space, space);
private static final long serialVersionUID = 6865098428255463649L;
/** /**
* Initializes a {@link JFrame} with UI elements used to send and read messages * Initializes a {@link JFrame} with UI elements used to send and read messages
* to different users. * to different users.
@ -73,14 +77,11 @@ public class ChatWindow extends JFrame {
gbl_contentPane.rowWeights = new double[] { 0.05, 1.0, 0.07 }; gbl_contentPane.rowWeights = new double[] { 0.05, 1.0, 0.07 };
contentPane.setLayout(gbl_contentPane); contentPane.setLayout(gbl_contentPane);
messageList.setCellRenderer(new MessageListRenderer()); // TODO: messageList.setFocusTraversalKeysEnabled(false);
messageList.setFocusTraversalKeysEnabled(false); // messageList.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT);
messageList.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT);
DefaultListModel<Message> messageListModel = new DefaultListModel<>(); // messageList.setFont(new Font("Arial", Font.PLAIN, 17));
messageList.setModel(messageListModel); // messageList.setFixedCellHeight(60);
messageList.setFont(new Font("Arial", Font.PLAIN, 17));
messageList.setFixedCellHeight(60);
messageList.setBorder(new EmptyBorder(space, space, space, space)); messageList.setBorder(new EmptyBorder(space, space, space, space));
scrollPane.setViewportView(messageList); scrollPane.setViewportView(messageList);
@ -91,7 +92,7 @@ public class ChatWindow extends JFrame {
gbc_scrollPane.gridx = 1; gbc_scrollPane.gridx = 1;
gbc_scrollPane.gridy = 1; gbc_scrollPane.gridy = 1;
gbc_scrollPane.insets = new Insets(space, space, space, space); gbc_scrollPane.insets = insets;
contentPane.add(scrollPane, gbc_scrollPane); contentPane.add(scrollPane, gbc_scrollPane);
// Message enter field // Message enter field
@ -100,8 +101,8 @@ public class ChatWindow extends JFrame {
@Override @Override
public void keyReleased(KeyEvent e) { public void keyReleased(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER if (e.getKeyCode() == KeyEvent.VK_ENTER
&& ((Settings.getInstance().isEnterToSend() && e.getModifiersEx() == 0) || (e.getModifiersEx() == KeyEvent.CTRL_DOWN_MASK))) && (Settings.getInstance().isEnterToSend() && e.getModifiersEx() == 0 || e.getModifiersEx() == KeyEvent.CTRL_DOWN_MASK))
postMessage(messageList); postMessage();
} }
}); });
@ -110,7 +111,7 @@ public class ChatWindow extends JFrame {
gbc_messageEnterTextfield.gridx = 1; gbc_messageEnterTextfield.gridx = 1;
gbc_messageEnterTextfield.gridy = 2; gbc_messageEnterTextfield.gridy = 2;
gbc_messageEnterTextfield.insets = new Insets(space, space, space, space); gbc_messageEnterTextfield.insets = insets;
contentPane.add(messageEnterTextArea, gbc_messageEnterTextfield); contentPane.add(messageEnterTextArea, gbc_messageEnterTextfield);
@ -121,9 +122,9 @@ public class ChatWindow extends JFrame {
gbc_moveSelectionPostButton.gridx = 2; gbc_moveSelectionPostButton.gridx = 2;
gbc_moveSelectionPostButton.gridy = 2; gbc_moveSelectionPostButton.gridy = 2;
gbc_moveSelectionPostButton.insets = new Insets(space, space, space, space); gbc_moveSelectionPostButton.insets = insets;
postButton.addActionListener((evt) -> { postMessage(messageList); }); postButton.addActionListener((evt) -> { postMessage(); });
contentPane.add(postButton, gbc_moveSelectionPostButton); contentPane.add(postButton, gbc_moveSelectionPostButton);
// Settings Button // Settings Button
@ -133,7 +134,7 @@ public class ChatWindow extends JFrame {
gbc_moveSelectionSettingsButton.gridx = 2; gbc_moveSelectionSettingsButton.gridx = 2;
gbc_moveSelectionSettingsButton.gridy = 0; gbc_moveSelectionSettingsButton.gridy = 0;
gbc_moveSelectionSettingsButton.insets = new Insets(space, space, space, space); gbc_moveSelectionSettingsButton.insets = insets;
settingsButton.addActionListener((evt) -> { settingsButton.addActionListener((evt) -> {
try { try {
@ -154,7 +155,7 @@ public class ChatWindow extends JFrame {
gbc_partnerName.gridx = 1; gbc_partnerName.gridx = 1;
gbc_partnerName.gridy = 0; gbc_partnerName.gridy = 0;
gbc_partnerName.insets = new Insets(space, space, space, space); gbc_partnerName.insets = insets;
contentPane.add(textPane, gbc_partnerName); contentPane.add(textPane, gbc_partnerName);
userList.setCellRenderer(new UserListRenderer()); userList.setCellRenderer(new UserListRenderer());
@ -165,17 +166,22 @@ public class ChatWindow extends JFrame {
final JList<User> selectedUserList = (JList<User>) listSelectionEvent.getSource(); final JList<User> selectedUserList = (JList<User>) listSelectionEvent.getSource();
final User user = selectedUserList.getSelectedValue(); final User user = selectedUserList.getSelectedValue();
// Select current chat
currentChat = localDB.getChats().stream().filter(chat -> chat.getRecipient().getId() == user.getId()).findFirst().get(); currentChat = localDB.getChats().stream().filter(chat -> chat.getRecipient().getId() == user.getId()).findFirst().get();
// Set all unread messages in the chat to read // Read current Chat
readCurrentChat(); currentChat.read();
// Set recipient in client and chat title
client.setRecipient(user); client.setRecipient(user);
textPane.setText(currentChat.getRecipient().getName()); textPane.setText(currentChat.getRecipient().getName());
// Update model and scroll down
messageList.setModel(currentChat.getModel()); messageList.setModel(currentChat.getModel());
scrollPane.setChatOpened(true); scrollPane.setChatOpened(true);
contentPane.revalidate();
revalidate();
repaint();
} }
}); });
@ -187,20 +193,29 @@ public class ChatWindow extends JFrame {
gbc_userList.gridx = 0; gbc_userList.gridx = 0;
gbc_userList.gridy = 1; gbc_userList.gridy = 1;
gbc_userList.anchor = GridBagConstraints.PAGE_START; gbc_userList.anchor = GridBagConstraints.PAGE_START;
gbc_userList.insets = new Insets(space, space, space, space); gbc_userList.insets = insets;
applyTheme(Settings.getInstance().getThemes().get(Settings.getInstance().getCurrentTheme())); applyTheme(Settings.getInstance().getThemes().get(Settings.getInstance().getCurrentTheme()));
contentPane.add(userList, gbc_userList); contentPane.add(userList, gbc_userList);
contentPane.revalidate(); contentPane.revalidate();
// Listen to theme changes
EventBus.getInstance().register(ThemeChangeEvent.class, (evt) -> applyTheme((Theme) evt.get())); EventBus.getInstance().register(ThemeChangeEvent.class, (evt) -> applyTheme((Theme) evt.get()));
contentPane.revalidate(); // Listen to received messages
EventBus.getInstance().register(MessageCreationEvent.class, (evt) -> {
Message message = ((MessageCreationEvent) evt).get();
localDB.getChats().stream().filter(c -> c.getRecipient().getId() == message.getSenderId()).findFirst().get().appendMessage(message);
revalidate();
repaint();
});
revalidate();
} }
/** /**
* Used to immediately reload the ChatWindow when settings were changed. * Used to immediately reload the {@link ChatWindow} when settings were changed.
* *
* @param theme the theme to change colors into * @param theme the theme to change colors into
* @since Envoy v0.2-alpha * @since Envoy v0.2-alpha
@ -210,8 +225,8 @@ public class ChatWindow extends JFrame {
contentPane.setBackground(theme.getBackgroundColor()); contentPane.setBackground(theme.getBackgroundColor());
contentPane.setForeground(theme.getUserNameColor()); contentPane.setForeground(theme.getUserNameColor());
// messageList // messageList
messageList.setSelectionForeground(theme.getUserNameColor()); // messageList.setSelectionForeground(theme.getUserNameColor());
messageList.setSelectionBackground(theme.getSelectionColor()); // messageList.setSelectionBackground(theme.getSelectionColor());
messageList.setForeground(theme.getMessageColorChat()); messageList.setForeground(theme.getMessageColorChat());
messageList.setBackground(theme.getCellColor()); messageList.setBackground(theme.getCellColor());
// scrollPane // scrollPane
@ -238,7 +253,7 @@ public class ChatWindow extends JFrame {
userList.setBackground(theme.getCellColor()); userList.setBackground(theme.getCellColor());
} }
private void postMessage(JList<Message> messageList) { private void postMessage() {
if (!client.hasRecipient()) { if (!client.hasRecipient()) {
JOptionPane.showMessageDialog(this, "Please select a recipient!", "Cannot send message", JOptionPane.INFORMATION_MESSAGE); JOptionPane.showMessageDialog(this, "Please select a recipient!", "Cannot send message", JOptionPane.INFORMATION_MESSAGE);
return; return;
@ -247,24 +262,32 @@ public class ChatWindow extends JFrame {
if (!messageEnterTextArea.getText().isEmpty()) try { if (!messageEnterTextArea.getText().isEmpty()) try {
// Create message // Create message
final Message message = new MessageBuilder(localDB.getUser().getId(), currentChat.getRecipient().getId()) final Message message = new MessageBuilder(localDB.getUser().getId(), currentChat.getRecipient().getId(), localDB.getIdGenerator())
.setText(messageEnterTextArea.getText()) .setText(messageEnterTextArea.getText())
.build(); .build();
// Send message // Send message
// TODO: Store offline messages
client.sendMessage(message); client.sendMessage(message);
// Add message to LocalDB and update UI // Add message to LocalDB and update UI
currentChat.appendMessage(message); currentChat.appendMessage(message);
messageList.setModel(currentChat.getModel()); // messageList.setModel(currentChat.getModel());
// Clear text field // Clear text field
messageEnterTextArea.setText(""); messageEnterTextArea.setText("");
contentPane.revalidate();
// Update UI
revalidate();
repaint();
// Request a new id generator if all ids were used
if (!localDB.getIdGenerator().hasNext()) client.requestIdGenerator();
} catch (Exception e) { } catch (Exception e) {
JOptionPane.showMessageDialog(this, JOptionPane.showMessageDialog(this,
"An exception occured while sending a message. See the log for more details.", "Error sending message:\n" + e.toString(),
"Exception occured", "Message sending error",
JOptionPane.ERROR_MESSAGE); JOptionPane.ERROR_MESSAGE);
e.printStackTrace(); e.printStackTrace();
} }
@ -291,64 +314,13 @@ public class ChatWindow extends JFrame {
} }
/** /**
* Updates the data model and the UI repeatedly after a certain amount of * Sets the {@link Client} used by this {@link ChatWindow}.
* 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);
}
// TODO: 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) {
// TODO: localDB.setMessagesToRead(currentChat);
}
}
/**
* Sets the {@link Client} used by this {@link ChatWindow}. If the client is
* online, the sync thread is started.
* *
* @param client the {@link Client} used to send and receive messages * @param client the {@link Client} used to send and receive messages
* @since Envoy v0.2-alpha * @since Envoy v0.2-alpha
*/ */
public void setClient(Client client) { public void setClient(Client client) {
this.client = client; this.client = client;
if (client.isOnline() && localDB != null) startSyncThread(Config.getInstance().getSyncTimeout());
} }
/** /**
@ -361,6 +333,5 @@ public class ChatWindow extends JFrame {
public void setLocalDB(LocalDB localDB) { public void setLocalDB(LocalDB localDB) {
this.localDB = localDB; this.localDB = localDB;
loadUsersAndChats(); loadUsersAndChats();
if (client != null && client.isOnline()) startSyncThread(Config.getInstance().getSyncTimeout());
} }
} }

View File

@ -1,13 +1,13 @@
package envoy.client.ui; package envoy.client.ui;
import java.awt.Component; import java.awt.Dimension;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import javax.swing.JLabel; import javax.swing.*;
import javax.swing.JList;
import javax.swing.ListCellRenderer;
import envoy.client.Settings; import envoy.client.Settings;
import envoy.client.ui.list.ComponentList;
import envoy.client.ui.list.ComponentListCellRenderer;
import envoy.data.Message; import envoy.data.Message;
/** /**
@ -21,22 +21,22 @@ import envoy.data.Message;
* @author Maximilian K&auml;fer * @author Maximilian K&auml;fer
* @since Envoy v0.1-alpha * @since Envoy v0.1-alpha
*/ */
public class MessageListRenderer extends JLabel implements ListCellRenderer<Message> { public class MessageListRenderer implements ComponentListCellRenderer<Message> {
private static final long serialVersionUID = 5164417379767181198L;
@Override @Override
public Component getListCellRendererComponent(JList<? extends Message> list, Message value, int index, boolean isSelected, boolean cellHasFocus) { public JComponent getListCellComponent(ComponentList<? extends Message> list, Message value, boolean isSelected) {
final JPanel panel = new JPanel();
if (isSelected) { if (isSelected) {
setBackground(list.getSelectionBackground()); panel.setBackground(Color.DARK_GRAY);
setForeground(list.getSelectionForeground()); panel.setForeground(Color.RED);
// TODO: Selection
// setBackground(list.getSelectionBackground());
// setForeground(list.getSelectionForeground());
} else { } else {
setBackground(list.getBackground()); panel.setBackground(list.getBackground());
setForeground(list.getForeground()); panel.setForeground(list.getForeground());
} }
setOpaque(true);
// TODO: Handle message attachments // TODO: Handle message attachments
final String text = value.getText(); final String text = value.getText();
@ -49,12 +49,22 @@ public class MessageListRenderer extends JLabel implements ListCellRenderer<Mess
// Getting the DateColor in the Chat of the current theme // Getting the DateColor in the Chat of the current theme
String dateColor = Settings.getInstance().getThemes().get(Settings.getInstance().getCurrentTheme()).getDateColorChat().toHex(); String dateColor = Settings.getInstance().getThemes().get(Settings.getInstance().getCurrentTheme()).getDateColorChat().toHex();
setText(String.format("<html><p style=\"color:%s\"><b><small>%s</b></small><br><p style=\"color:%s\">%s :%s</html>", panel.add(new JLabel(String.format("<html><p style=\"color:%s\"><b><small>%s</b></small><br><p style=\"color:%s\">%s :%s</html>",
dateColor, dateColor,
date, date,
textColor, textColor,
text, text,
state)); state)));
return this;
// Define some space to the messages below
panel.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEmptyBorder(0, 0, 15, 0), BorderFactory.createEtchedBorder()));
// Define a maximum height of 50px
Dimension size = new Dimension(list.getWidth() - 25, 50);
panel.setMaximumSize(size);
panel.setMinimumSize(size);
panel.setPreferredSize(size);
return panel;
} }
} }

View File

@ -62,7 +62,7 @@ public class Startup {
if (!config.isInitialized()) throw new EnvoyException("Server or port are not defined"); if (!config.isInitialized()) throw new EnvoyException("Server or port are not defined");
} catch (Exception e) { } catch (Exception e) {
JOptionPane JOptionPane
.showMessageDialog(null, "Error loading configuration values: \n" + e.toString(), "Configuration error", JOptionPane.ERROR_MESSAGE); .showMessageDialog(null, "Error loading configuration values:\n" + e.toString(), "Configuration error", JOptionPane.ERROR_MESSAGE);
System.exit(1); System.exit(1);
e.printStackTrace(); e.printStackTrace();
} }
@ -97,7 +97,8 @@ public class Startup {
Client client = new Client(); Client client = new Client();
try { try {
// Try entering online mode first // Try entering online mode first
client.onlineInit(credentials); localDB.loadIdGenerator();
client.onlineInit(credentials, localDB);
} catch (Exception e1) { } catch (Exception e1) {
logger.warning("Could not connect to server. Trying offline mode..."); logger.warning("Could not connect to server. Trying offline mode...");
e1.printStackTrace(); e1.printStackTrace();
@ -152,7 +153,7 @@ public class Startup {
.getItems() .getItems()
.get("onCloseMode") .get("onCloseMode")
.setChangeHandler((onCloseMode) -> chatWindow .setChangeHandler((onCloseMode) -> chatWindow
.setDefaultCloseOperation((boolean) onCloseMode ? JFrame.HIDE_ON_CLOSE : JFrame.EXIT_ON_CLOSE)); .setDefaultCloseOperation((Boolean) onCloseMode ? JFrame.HIDE_ON_CLOSE : JFrame.EXIT_ON_CLOSE));
} catch (EnvoyException e) { } catch (EnvoyException e) {
logger.warning("The StatusTrayIcon is not supported on this platform!"); logger.warning("The StatusTrayIcon is not supported on this platform!");
} }

View File

@ -0,0 +1,91 @@
package envoy.client.ui.list;
import javax.swing.BoxLayout;
import javax.swing.JPanel;
/**
* Provides a vertical list layout of components provided in a
* {@link ComponentListModel}. Similar to {@link javax.swing.JList} but capable
* of rendering {@link JPanel}s.<br>
* <br>
* Project: <strong>envoy-client</strong><br>
* File: <strong>ComponentList.java</strong><br>
* Created: <strong>25.01.2020</strong><br>
*
* @param <E> the type of object displayed in this list
* @author Kai S. K. Engelbart
* @since Envoy v0.3-alpha
*/
public class ComponentList<E> extends JPanel {
private ComponentListModel<E> model;
private ComponentListCellRenderer<E> renderer;
private static final long serialVersionUID = 1759644503942876737L;
/**
* Creates an instance of {@link ComponentList}.
*
* @param renderer the list cell renderer used to display elements provided by
* the {@link ComponentListModel}
* @since Envoy v0.3-alpha
*/
public ComponentList(ComponentListCellRenderer<E> renderer) {
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
this.renderer = renderer;
}
/**
* Creates an instance of {@link ComponentList}.
*
* @param model the list model providing the list elements to render
* @param renderer the list cell renderer used to display elements provided by
* the {@link ComponentListModel}
* @since Envoy v0.3-alpha
*/
public ComponentList(ComponentListModel<E> model, ComponentListCellRenderer<E> renderer) {
this(renderer);
this.model = model;
setModel(model);
}
/**
* Sets the list model providing the list elements to render
*
* @param model the list model to set
* @since Envoy v0.3-alpha
*/
public void setModel(ComponentListModel<E> model) {
// Remove old model
if (this.model != null)
this.model.setComponentList(null);
// Synchronize with new model
this.model = model;
this.model.setComponentList(this);
synchronizeModel();
}
/**
* Adds an object to the list by rendering it with the current
* {@link ComponentListCellRenderer}.
*
* @param elem the element to add
* @since Envoy v0.3-alpha
*/
void add(E elem) {
add(renderer.getListCellComponent(this, elem, false));
}
/**
* Removes all child components and then adds all components representing the
* elements of the {@link ComponentListModel}.
*
* @since Envoy v0.3-alpha
*/
void synchronizeModel() {
removeAll();
if (model != null) for (E elem : model)
add(elem);
}
}

View File

@ -0,0 +1,30 @@
package envoy.client.ui.list;
import javax.swing.JComponent;
/**
* Allows a {@link ComponentList} convert its elements into Swing components
* that can be rendered.<br>
* <br>
* Project: <strong>envoy-client</strong><br>
* File: <strong>ComponentListCellRenderer.java</strong><br>
* Created: <strong>25.01.2020</strong><br>
*
* @param <E> the type of object displayed in this list
* @author Kai S. K. Engelbart
* @since Envoy v0.3-alpha
*/
public interface ComponentListCellRenderer<E> {
/**
* Provides a Swing component representing a list element.
*
* @param list the list in which the component will be displayed
* @param value the list element that will be converted
* @param isSelected {@code true} if the user has selected the list cell in
* which the list element is rendered
* @return the component representing the list element
* @since Envoy v0.3-alpha
*/
JComponent getListCellComponent(ComponentList<? extends E> list, E value, boolean isSelected);
}

View File

@ -0,0 +1,111 @@
package envoy.client.ui.list;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Stores objects that will be displayed in a {@link ComponentList}.<br>
* <br>
* Project: <strong>envoy-client</strong><br>
* File: <strong>ComponentListModel.java</strong><br>
* Created: <strong>25.01.2020</strong><br>
*
* @param <E> the type of object displayed in this list
* @author Kai S. K. Engelbart
* @since Envoy v0.3-alpha
*/
public final class ComponentListModel<E> implements Iterable<E>, Serializable {
private List<E> elements = new ArrayList<>();
transient private ComponentList<E> componentList;
private static final long serialVersionUID = 4815005915255497331L;
/**
* Adds an element to this model and notifies the associated
* {@link ComponentList} to add the corresponding component.
*
* @param e the element to add
* @return {@code true}
* @see java.util.List#add(java.lang.Object)
* @since Envoy v0.3-alpha
*/
public boolean add(E e) {
if (componentList != null) componentList.add(e);
return elements.add(e);
}
/**
* Removes all elements from this model and clears the associated
* {@link ComponentList}.
*
* @see java.util.List#clear()
* @since Envoy v0.3-alpha
*/
public void clear() {
elements.clear();
if (componentList != null) componentList.removeAll();
}
/**
* @param index the index to retrieve the element from
* @return the element located at the index
* @see java.util.List#get(int)
* @since Envoy v0.3-alpha
*/
public E get(int index) { return elements.get(index); }
/**
* Removes the element at a specific index from this model and the corresponding
* component from the {@link ComponentList}.
*
* @param index the index of the element to remove
* @return the removed element
* @see java.util.List#remove(int)
* @since Envoy v0.3-alpha
*/
public E remove(int index) {
if (componentList != null) componentList.remove(index);
return elements.remove(index);
}
/**
* @return the amount of elements in this list model
* @see java.util.List#size()
* @since Envoy v0.3-alpha
*/
public int size() { return elements.size(); }
/**
* @return an iterator over the elements of this list model
* @see java.util.List#iterator()
* @since Envoy v0.3-alpha
*/
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
Iterator<E> iter = elements.iterator();
@Override
public boolean hasNext() { return iter.hasNext(); }
@Override
public E next() { return iter.next(); }
};
}
/**
* Sets the component list displaying the elements of this model and triggers a
* synchronization.
*
* @param componentList the component list to set
* @since Envoy v0.3-alpha
*/
void setComponentList(ComponentList<E> componentList) {
this.componentList = componentList;
if (componentList != null) componentList.synchronizeModel();
}
}