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.