diff --git a/src/main/java/envoy/client/ui/ContextMenu.java b/src/main/java/envoy/client/ui/ContextMenu.java
new file mode 100644
index 0000000..7328b0d
--- /dev/null
+++ b/src/main/java/envoy/client/ui/ContextMenu.java
@@ -0,0 +1,184 @@
+package envoy.client.ui;
+
+import java.awt.Component;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.swing.*;
+
+/**
+ * This class defines a menu that will be automatically called if
+ * {@link MouseEvent#isPopupTrigger()} returns true for the parent component.
+ * The user has the possibility to directly add actions to be performed when
+ * clicking on the element with the selected String. Additionally, for each
+ * element an {@link Icon} can be added, but it must not be.
+ * If the key(text) of an element starts with one of the predefined values, a
+ * special component will be called: either a {@link JRadioButtonMenuItem}, a
+ * {@link JCheckBoxMenuItem} or a {@link JMenu} will be created.
+ *
+ * Project: envoy-client
+ * File: ContextMenu.java
+ * Created: 17 Mar 2020
+ *
+ * @author Leon Hofmeister
+ * @since Envoy v0.1-beta
+ */
+public class ContextMenu extends JPopupMenu {
+
+ private static final long serialVersionUID = 2177146471226992104L;
+
+ /**
+ * If a key starts with this String, a {@link JCheckBoxMenuItem} will be created
+ */
+ public static final String checkboxMenuItem = "ChBoMI";
+ /**
+ * If a key starts with this String, a {@link JRadioButtonMenuItem} will be
+ * created
+ */
+ public static final String radioButtonMenuItem = "RaBuMI";
+ /**
+ * If a key starts with this String, a {@link JMenu} will be created
+ */
+ public static final String subMenuItem = "SubMI";
+
+ private Map items = new HashMap<>();
+ private Map icons = new HashMap<>();
+
+ private ButtonGroup radioButtonGroup = new ButtonGroup();
+ private boolean built = false;
+ private boolean visible = false;
+
+ /**
+ * @since Envoy v0.1-beta
+ */
+ public ContextMenu() {}
+
+ /**
+ * @param label the string that a UI may use to display as a title
+ * for the pop-up menu
+ * @param parent the component which will call this
+ * {@link ContextMenu}
+ * @param itemsWithActions a map of all strings to be displayed with according
+ * actions
+ * @param itemIcons the icons to be displayed before a name, if wanted.
+ * Only keys in here will have an Icon displayed. More
+ * precisely, all keys here not included in the first
+ * map will be thrown out.
+ * @since Envoy v0.1-beta
+ */
+ public ContextMenu(String label, Component parent, Map itemsWithActions, Map itemIcons) {
+ super(label);
+ setInvoker(parent);
+ this.items = (itemsWithActions != null) ? itemsWithActions : items;
+ this.icons = (itemIcons != null) ? itemIcons : icons;
+ }
+
+ /**
+ * Prepares the PopupMenu to be displayed. Should only be used once all map
+ * values have been set.
+ *
+ * @return this instance of {@link ContextMenu} to allow chaining behind the
+ * constructor
+ * @since Envoy v0.1-beta
+ */
+ public ContextMenu build() {
+ items.forEach((text, action) -> {
+ // case radio button wanted
+ if (text.startsWith(radioButtonMenuItem)) {
+ var item = new JRadioButtonMenuItem(text.substring(radioButtonMenuItem.length()), icons.containsKey(text) ? icons.get(text) : null);
+ item.addActionListener(action);
+ radioButtonGroup.add(item);
+ add(item);
+ // case check box wanted
+ } else if (text.startsWith(checkboxMenuItem)) {
+ var item = new JCheckBoxMenuItem(text.substring(checkboxMenuItem.length()), icons.containsKey(text) ? icons.get(text) : null);
+ item.addActionListener(action);
+ add(item);
+ // case sub-menu wanted
+ } else if (text.startsWith(subMenuItem)) {
+ var item = new JMenu(text.substring(subMenuItem.length()));
+ item.addActionListener(action);
+ add(item);
+ } else {
+ // normal JMenuItem wanted
+ var item = new JMenuItem(text, icons.containsKey(text) ? icons.get(text) : null);
+ item.addActionListener(action);
+ add(item);
+ }
+ });
+ getInvoker().addMouseListener(getShowingListener());
+ built = true;
+ return this;
+ }
+
+ /**
+ * @param label the string that a UI may use to display as a title for the
+ * pop-up menu.
+ * @since Envoy v0.1-beta
+ */
+ public ContextMenu(String label) { super(label); }
+
+ private MouseAdapter getShowingListener() {
+ return new MouseAdapter() {
+
+ @Override
+ public void mouseClicked(MouseEvent e) { action(e); }
+
+ @Override
+ public void mousePressed(MouseEvent e) { action(e); }
+
+ @Override
+ public void mouseReleased(MouseEvent e) { action(e); }
+
+ private void action(MouseEvent e) {
+ if (!built) build();
+ if (e.isPopupTrigger()) {
+ show(e.getComponent(), e.getX(), e.getY());
+ // hides the menu if already visible
+ visible = !visible;
+ setVisible(visible);
+
+ }
+ }
+ };
+ }
+
+ /**
+ * Removes all subcomponents of this menu.
+ *
+ * @since Envoy v0.1-beta
+ */
+ public void clear() {
+ removeAll();
+ items = new HashMap<>();
+ icons = new HashMap<>();
+ }
+
+ /**
+ * @return the items
+ * @since Envoy v0.1-beta
+ */
+ public Map getItems() { return items; }
+
+ /**
+ * @param items the items with the displayed text and the according action to
+ * take once called
+ * @since Envoy v0.1-beta
+ */
+ public void setItems(Map items) { this.items = items; }
+
+ /**
+ * @return the icons
+ * @since Envoy v0.1-beta
+ */
+ public Map getIcons() { return icons; }
+
+ /**
+ * @param icons the icons to set
+ * @since Envoy v0.1-beta
+ */
+ public void setIcons(Map icons) { this.icons = icons; }
+}
diff --git a/src/main/java/envoy/client/ui/Startup.java b/src/main/java/envoy/client/ui/Startup.java
index 8f44c2a..5c03881 100644
--- a/src/main/java/envoy/client/ui/Startup.java
+++ b/src/main/java/envoy/client/ui/Startup.java
@@ -15,6 +15,8 @@ import javax.swing.SwingUtilities;
import envoy.client.data.*;
import envoy.client.net.Client;
import envoy.client.net.WriteProxy;
+import envoy.client.ui.container.ChatWindow;
+import envoy.client.ui.container.LoginDialog;
import envoy.data.Config;
import envoy.data.Message;
import envoy.data.User.UserStatus;
diff --git a/src/main/java/envoy/client/ui/ChatWindow.java b/src/main/java/envoy/client/ui/container/ChatWindow.java
similarity index 92%
rename from src/main/java/envoy/client/ui/ChatWindow.java
rename to src/main/java/envoy/client/ui/container/ChatWindow.java
index 182f824..fc43f89 100644
--- a/src/main/java/envoy/client/ui/ChatWindow.java
+++ b/src/main/java/envoy/client/ui/container/ChatWindow.java
@@ -1,8 +1,12 @@
-package envoy.client.ui;
+package envoy.client.ui.container;
import java.awt.*;
+import java.awt.datatransfer.StringSelection;
import java.awt.event.*;
import java.io.IOException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -18,13 +22,13 @@ import envoy.client.event.MessageCreationEvent;
import envoy.client.event.ThemeChangeEvent;
import envoy.client.net.Client;
import envoy.client.net.WriteProxy;
+import envoy.client.ui.Theme;
import envoy.client.ui.list.ComponentList;
import envoy.client.ui.list.ComponentListModel;
import envoy.client.ui.primary.PrimaryButton;
import envoy.client.ui.primary.PrimaryScrollPane;
import envoy.client.ui.primary.PrimaryTextArea;
import envoy.client.ui.renderer.ContactsSearchRenderer;
-import envoy.client.ui.renderer.MessageListRenderer;
import envoy.client.ui.renderer.UserListRenderer;
import envoy.client.ui.settings.SettingsScreen;
import envoy.data.Message;
@@ -103,7 +107,6 @@ 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")));
@@ -123,19 +126,39 @@ public class ChatWindow extends JFrame {
@Override
public void mouseClicked(MouseEvent e) {
if (e.isPopupTrigger()) {
+ contextMenu = new JPopupMenu("message options");
+ Map commands = new HashMap<>() {
+ private static final long serialVersionUID = -2755235774946990126L;
+
+ {
+ put("forward selected message",
+ evt -> forwardMessageToMultipleUsers(messageList.getSelected().get(0),
+ ContactsChooserDialog
+ .showForwardingDialog("Forward selected message to", messageList.getSelected().get(0), client)));
+ put("copy", evt -> {
+ // TODO should be enhanced to allow also copying of message attachments,
+ // especially pictures
+ StringSelection copy = new StringSelection(messageList.getSelected().get(0).getText());
+ Toolkit.getDefaultToolkit().getSystemClipboard().setContents(copy, copy);
+ });
+ // TODO insert implementation to edit and delete messages
+ put("delete", evt -> {});
+ put("edit", evt -> {});
+ put("quote", evt -> {});
+ }
+ };
+ commands.forEach((name, action) -> { var item = new JMenuItem(name); item.addActionListener(action); contextMenu.add(item); });
+ contextMenu.show(e.getComponent(), e.getX(), e.getY());
}
}
});
scrollPane.setViewportView(messageList);
scrollPane.addComponentListener(new ComponentAdapter() {
- // Update list elements when scroll pane (and thus list) is resized
+ // updates list elements when list is resized
@Override
- public void componentResized(ComponentEvent e) {
- messageList.setMaximumSize(new Dimension(scrollPane.getWidth(), Integer.MAX_VALUE));
- messageList.synchronizeModel();
- }
+ public void componentResized(ComponentEvent e) { messageList.synchronizeModel(); }
});
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
@@ -538,6 +561,15 @@ public class ChatWindow extends JFrame {
sendMessage(new MessageBuilder(msg, recipient.getId(), localDb.getIdGenerator()).build());
}
+ private void forwardMessageToMultipleUsers(Message message, Collection recipients) {
+ recipients.forEach(recipient -> forwardMessage(message, recipient));
+ }
+
+ @SuppressWarnings("unused")
+ private void forwardMultipleMessagesToMultipleUsers(Collection messages, Collection recipients) {
+ messages.forEach(message -> forwardMessageToMultipleUsers(message, recipients));
+ }
+
/**
* Sends a {@link Message} to the server.
*
@@ -612,8 +644,6 @@ 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/container/ContactsChooserDialog.java b/src/main/java/envoy/client/ui/container/ContactsChooserDialog.java
new file mode 100644
index 0000000..b82707b
--- /dev/null
+++ b/src/main/java/envoy/client/ui/container/ContactsChooserDialog.java
@@ -0,0 +1,125 @@
+package envoy.client.ui.container;
+
+import java.awt.BorderLayout;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JPanel;
+import javax.swing.border.EmptyBorder;
+
+import envoy.client.data.Settings;
+import envoy.client.net.Client;
+import envoy.client.ui.Theme;
+import envoy.client.ui.list.ComponentList;
+import envoy.client.ui.list.ComponentListModel;
+import envoy.client.ui.renderer.MessageListRenderer;
+import envoy.client.ui.renderer.UserComponentListRenderer;
+import envoy.data.Message;
+import envoy.data.User;
+
+/**
+ * This class defines a dialog to choose contacts from.
+ *
+ * Project: envoy-client
+ * File: ContactsChooserDialog.java
+ * Created: 15 Mar 2020
+ *
+ * @author Leon Hofmeister
+ * @since Envoy v0.1-beta
+ */
+public class ContactsChooserDialog extends JDialog {
+
+ private static final long serialVersionUID = -5774558118579032256L;
+
+ private ComponentList contactList = new ComponentList<>(new UserComponentListRenderer());
+ private JButton okButton = new JButton("Ok");
+ private JButton cancelButton = new JButton("Cancel");
+
+ private final Theme theme = Settings.getInstance().getTheme(Settings.getInstance().getCurrentTheme());
+
+ private final JPanel contentPanel = new JPanel();
+
+ /**
+ * Shows a modal contacts-chooser dialog and blocks until the
+ * dialog is hidden. If the user presses the "OK" button, then
+ * this method hides/disposes the dialog and returns the selected element (has
+ * yet
+ * to be casted back to its original type due to the limitations of Generics in
+ * Java).
+ * If the user presses the "Cancel" button or closes the dialog without
+ * pressing "OK", then this method disposes the dialog and returns an empty
+ * ArrayList
.
+ *
+ * @param title the title of the dialog
+ * @param message the {@link Message} to display on top of the Dialog
+ * @param client the client whose contacts should be displayed
+ * @return the selected Element (yet has to be casted to the wanted type due to
+ * the Generics limitations in Java)
+ * @since Envoy v0.1-beta
+ */
+ public static List showForwardingDialog(String title, Message message, Client client) {
+ ContactsChooserDialog dialog = new ContactsChooserDialog();
+ dialog.setTitle(title);
+ dialog.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
+ dialog.addCancelButtonActionListener(e -> dialog.dispose());
+ dialog.getContentPanel()
+ .add(new MessageListRenderer(client.getSender().getId()).getListCellComponent(null, message, false), BorderLayout.NORTH);
+ List results = new ArrayList<>();
+ dialog.addOkButtonActionListener(e -> { results.addAll(dialog.getContactList().getSelected()); dialog.dispose(); });
+ ComponentListModel contactListModel = dialog.getContactList().getModel();
+ client.getContacts().getContacts().forEach(user -> contactListModel.add(user));
+ dialog.setVisible(true);
+ dialog.repaint();
+ dialog.revalidate();
+ return results.size() > 0 ? results : null;
+ }
+
+ /**
+ * @since Envoy v0.1-beta
+ */
+ private ContactsChooserDialog() {
+ contactList.enableMultipleSelection();
+ // setBounds(100, 100, 450, 300);
+ setModal(true);
+ getContentPane().setLayout(new BorderLayout());
+ setBackground(theme.getBackgroundColor());
+ setForeground(theme.getMessageTextColor());
+ contentPanel.setBorder(new EmptyBorder(5, 5, 5, 5));
+ getContentPane().add(contentPanel, BorderLayout.CENTER);
+ contentPanel.setLayout(new BorderLayout(0, 0));
+ contentPanel.add(contactList, BorderLayout.CENTER);
+ {
+ JPanel buttonPane = new JPanel();
+ getContentPane().add(buttonPane, BorderLayout.SOUTH);
+ {
+ JButton okButton = new JButton("OK");
+ okButton.setMnemonic(KeyEvent.VK_ENTER);
+ okButton.setActionCommand("OK");
+ buttonPane.setLayout(new BorderLayout(0, 0));
+ buttonPane.add(okButton, BorderLayout.EAST);
+ getRootPane().setDefaultButton(okButton);
+ }
+ {
+ JButton cancelButton = new JButton("Cancel");
+ cancelButton.setActionCommand("Cancel");
+ buttonPane.add(cancelButton, BorderLayout.WEST);
+ }
+ }
+ }
+
+ /**
+ * @return the underlying {@link ComponentList}
+ * @since Envoy v0.1-beta
+ */
+ private ComponentList getContactList() { return contactList; }
+
+ private void addOkButtonActionListener(ActionListener l) { okButton.addActionListener(l); }
+
+ private void addCancelButtonActionListener(ActionListener l) { cancelButton.addActionListener(l); }
+
+ private JPanel getContentPanel() { return contentPanel; }
+}
diff --git a/src/main/java/envoy/client/ui/LoginDialog.java b/src/main/java/envoy/client/ui/container/LoginDialog.java
similarity index 99%
rename from src/main/java/envoy/client/ui/LoginDialog.java
rename to src/main/java/envoy/client/ui/container/LoginDialog.java
index 9cd645d..51d9817 100644
--- a/src/main/java/envoy/client/ui/LoginDialog.java
+++ b/src/main/java/envoy/client/ui/container/LoginDialog.java
@@ -1,4 +1,4 @@
-package envoy.client.ui;
+package envoy.client.ui.container;
import java.awt.*;
import java.awt.event.ItemEvent;
@@ -16,6 +16,7 @@ import javax.swing.border.EmptyBorder;
import envoy.client.data.*;
import envoy.client.event.HandshakeSuccessfulEvent;
import envoy.client.net.Client;
+import envoy.client.ui.Theme;
import envoy.client.ui.primary.PrimaryButton;
import envoy.data.LoginCredentials;
import envoy.data.Message;
diff --git a/src/main/java/envoy/client/ui/container/package-info.java b/src/main/java/envoy/client/ui/container/package-info.java
new file mode 100644
index 0000000..27645f4
--- /dev/null
+++ b/src/main/java/envoy/client/ui/container/package-info.java
@@ -0,0 +1,13 @@
+/**
+ * This package contains all graphical Containers, like Dialogs and Frames.
+ *
+ * Project: envoy-client
+ * File: package-info.java
+ * Created: 16 Mar 2020
+ *
+ * @author Leon Hofmeister
+ * @author Kai S. K. Engelbart
+ * @author Maximilian Käfer
+ * @since Envoy v0.1-beta
+ */
+package envoy.client.ui.container;
diff --git a/src/main/java/envoy/client/ui/list/ComponentList.java b/src/main/java/envoy/client/ui/list/ComponentList.java
index 3d90ea9..b6962e3 100644
--- a/src/main/java/envoy/client/ui/list/ComponentList.java
+++ b/src/main/java/envoy/client/ui/list/ComponentList.java
@@ -108,6 +108,10 @@ public class ComponentList extends JPanel {
* @since Envoy v0.1-beta
*/
private void add(E elem, int index, boolean isSelected) {
+ if (isSelected && !multipleSelectionEnabled) {
+ clearSelections();
+ currentSelections.add(index);
+ }
final JComponent component = renderer.getListCellComponent(this, elem, isSelected);
component.addMouseListener(getSelectionListener(index));
add(component, index);
@@ -169,7 +173,7 @@ public class ComponentList extends JPanel {
* @since Envoy v0.1-beta
*/
private void clearSelections() {
- currentSelections.forEach(index2 -> updateSelection(index2, false));
+ currentSelections.forEach(index -> updateSelection(index, false));
currentSelections.clear();
}
@@ -224,7 +228,10 @@ public class ComponentList extends JPanel {
* the component list
* @since Envoy v0.1-beta
*/
- public void setMultipleSelectionEnabled(boolean multipleSelectionEnabled) { this.multipleSelectionEnabled = multipleSelectionEnabled; }
+ public void setMultipleSelectionEnabled(boolean multipleSelectionEnabled) {
+ this.multipleSelectionEnabled = multipleSelectionEnabled;
+ if (!multipleSelectionEnabled) clearSelections();
+ }
/**
* Enables the selection of multiple elements.